Config
Auth

Authentication

AIOpen as plain markdown for AI

Each project can have one or more authentication strategies. When any strategy is defined, every request to the project must satisfy at least one of them. eRPC auto-detects which strategy applies based on the request payload (e.g. presence of tokensecret, an Authorization: Bearer … header ⇒ jwt, etc.).

Supported strategies:

  • secret — static API key (backend-to-backend)
  • network — IP / CIDR allowlist
  • jwt — JSON Web Token, public-key verified (recommended for frontends)
  • siwe — Sign-in-with-Ethereum signed message
  • x402 — pay-per-request via stablecoin (HTTP 402)
  • database — used internally for API-key authentication; see Admin API key management for the recommended workflow. This strategy is not configurable via YAML/TS.

Each strategy entry also supports:

  • ignoreMethods / allowMethods — restrict which RPC methods this strategy authorizes
  • rateLimitBudget — bind a rate-limit budget to requests that satisfy this strategy

Minimum useful config — a secret for backend traffic

The smallest auth setup: one secret token for backend-to-backend.

projectsauthstrategies[]
erpc.yaml
projects:  - id: main    auth:      strategies:        - type: secret          rateLimitBudget: premium      # optional; bind to a budget defined under rateLimiters          secret:            id: backend                  # optional label used in metrics            value: ${MY_SECRET_VALUE}   # env var interpolation works

The client must send the secret as a query string parameter or a header:

# Query string:
curl -X POST 'https://erpc.example/main/evm/1?secret=...'
 
# Header:
curl -X POST 'https://erpc.example/main/evm/1' \
  -H 'X-ERPC-Secret-Token: ...'

Picking a strategy

If you want to…Use
Backend-to-backend; only your servers hit eRPCsecret (use env vars for the value)
Lock down to specific datacenters / VPNs / public IPsnetwork
Frontend dApps with per-user rate limits and expirationsjwt
Wallet-signed access without running an issuersiwe
Charge per request via stablecoinx402

You can stack multiple strategies — each request only needs to satisfy ONE. Apply ignoreMethods / allowMethods per strategy to scope what each one authorizes.

Copy for your AI assistant — full authentication referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.

AuthConfig — top-level fields

FieldTypeNotes
strategies[]AuthStrategyConfig[]Each entry is one strategy. Multiple strategies are OR'd: a request is authorized when ANY strategy accepts it.

AuthStrategyConfig — fields shared by every strategy

FieldTypeNotes
type"secret"|"network"|"jwt"|"siwe"|"x402"|"database"Required. Selects the strategy. database is managed via the Admin API — not configurable via YAML/TS.
ignoreMethodsstring[]Block these methods (matcher syntax: *, |, !).
allowMethodsstring[]Allowlist; when set, blocks everything not listed. When allowMethods is set and ignoreMethods is not, ignoreMethods: ["*"] is implicit.
rateLimitBudgetstringBind requests authorized by this strategy to a budget defined under top-level rateLimiters.budgets[].
Strategy-specific blockobjectOne of secret, network, jwt, siwe, or x402 — see per-strategy reference below.

secret strategy — all fields

auth:
  strategies:
    - type: secret
      ignoreMethods: ["alchemy_*"]
      allowMethods: ["eth_*", "net_*"]
      rateLimitBudget: premium
      secret:
        id: backend-key             # optional label, surfaces in metric labels
        value: ${MY_SECRET_VALUE}    # env-var expansion at config load
FieldNotes
secret.idOptional label. Used as a metric label so multiple secret strategies can be told apart in dashboards.
secret.valueThe static token. Required. Use env-var interpolation (${VAR} in YAML, process.env.VAR in TS) to keep secrets out of the config.
secret.rateLimitBudgetPer-secret rate-limit budget (overrides the strategy-level budget for this secret).

Client sends via ?secret=... query string OR X-ERPC-Secret-Token: ... header.

⚠️

Secrets sent from a browser are visible to users. Only use secret strategy for backend traffic. For frontends, prefer jwt (per-user, expiring) or siwe (wallet-signed).

network strategy — all fields

auth:
  strategies:
    - type: network
      network:
        allowLocalhost: true                     # accept 127.0.0.1 / ::1 / loopback
        allowedIPs: ["89.123.123.123"]            # explicit IP allowlist
        allowedCIDRs: ["78.13.0.0/16"]            # CIDR ranges
        trustedProxies: ["10.0.0.0/8"]            # see X-Forwarded-For rules below
        ipAsUser: true                            # use client IP as the rate-limit user ID
        rateLimitBudget: ip-tier
FieldNotes
allowLocalhostWhen true, requests from 127.0.0.1, ::1, and other loopback addresses are accepted unconditionally.
allowedIPs[]List of literal IPs. Both IPv4 and IPv6 accepted.
allowedCIDRs[]List of CIDR ranges. e.g. 78.13.0.0/16, 2001:db8::/32.
trustedProxies[]IPs or CIDRs that are trusted to set X-Forwarded-For. eRPC walks the X-F-F list left-to-right and picks the first IP that is NOT in trustedProxies. See "X-Forwarded-For semantics" below.
ipAsUserWhen true, the client's IP becomes their per-user identifier for rate limiting — rateLimitBudget then bills per IP rather than across the whole strategy.
rateLimitBudgetPer-strategy rate-limit budget.

X-Forwarded-For semantics:

Request:    X-Forwarded-For: 192.168.1.123, 22.22.22.22, 33.33.33.33
trustedProxies: ["192.168.1.123"]
=> Detected client IP: 22.22.22.22  (first non-trusted)

Request:    X-Forwarded-For: 11.11.11.11, 22.22.22.22, 33.33.33.33
trustedProxies: ["192.168.1.123"]
=> Detected client IP: 11.11.11.11  (already non-trusted)

This pairs with the server-level server.trustedIPForwarders and server.trustedIPHeaders — both must be set up if you're behind a load balancer.

jwt strategy — all fields

auth:
  strategies:
    - type: jwt
      jwt:
        verificationKeys:
          "rsa-kid-1": "file:///etc/erpc/public_key.pem"
          "rsa-kid-2": "${MY_RSA_KEY_2_PEM}"
        allowedIssuers: ["https://issuer.example.com"]
        allowedAudiences: ["https://my-dapp.example.com"]
        allowedAlgorithms: ["RS256", "ES256"]
        requiredClaims: ["sub", "role"]
        rateLimitBudgetClaimName: rlm        # default: "rlm"
FieldNotes
verificationKeysMap of kid (key ID matching the JWT header's kid) → public key. Value is either a PEM string or a file:// path. At least one is required.
allowedIssuers[]If set, the JWT's iss claim must match one of these.
allowedAudiences[]If set, the JWT's aud claim must match.
allowedAlgorithms[]Whitelist of alg header values (e.g. RS256, ES256, HS256).
requiredClaims[]List of claim names that must be present in the JWT payload (any value).
rateLimitBudgetClaimNameThe JWT claim whose value selects the rate-limit budget. Default "rlm". The claim's value must match a budget ID in rateLimiters.budgets[].

JWT expiration (exp claim) is enforced — expired tokens are rejected. Algorithm-confusion attacks (e.g. switching from RS256 to HS256 with the public key as the HMAC secret) are prevented by allowedAlgorithms.

JWT is the recommended strategy for frontend dApps — per-user, expiring, supports per-user rate-limit budgets via the claim mechanism.

siwe strategy — all fields

auth:
  strategies:
    - type: siwe
      siwe:
        allowedDomains: ["my-dapp.example.com"]
        rateLimitBudget: siwe-tier
FieldNotes
allowedDomains[]List of domains the SIWE message's Domain field must match.
rateLimitBudgetPer-strategy rate-limit budget.

Client sends via query string OR headers:

# Query string:
curl -X POST 'https://erpc.example/main/evm/1?message=...&signature=0x...'
 
# Headers:
curl -X POST 'https://erpc.example/main/evm/1' \
  -H 'X-ERPC-SIWE-Message: <base64-message>' \
  -H 'X-ERPC-SIWE-Signature: 0x...'

The SIWE message must be base64-encoded. eRPC verifies that the signature matches the message and that the message's Domain is in allowedDomains. The recovered wallet address becomes the user ID for rate limiting.

x402 strategy — all fields

auth:
  strategies:
    - type: x402
      x402:
        facilitatorUrl: https://x402.org/facilitator
        sellerAddress: 0xYourWalletAddress
        pricePerRequest: "1"                   # atomic units; "1" = 0.000001 USDC at 6 decimals
        network: "eip155:8453"                  # Base mainnet
        asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"  # optional, defaults to USDC
        scheme: exact                           # only "exact" today; "upto" planned
        description: "My RPC endpoint"
        maxTimeoutSeconds: 300                  # payment auth validity window; default 300
        rateLimitBudget: x402-tier              # per-payer rate-limit
        verifyOnly: false                       # true = skip settlement; for testing
        extra:                                   # merged into the 402 payment requirements
          name: "USDC"
          version: "2"
FieldRequiredNotes
facilitatorUrlx402 facilitator endpoint that handles verify and settle ops.
sellerAddressWallet address that receives payments.
pricePerRequestCost per request in atomic units of asset. For USDC (6 decimals), "1" = $0.000001.
networkx402 network identifier — eip155:8453 for Base mainnet, eip155:84532 for Base Sepolia.
assetToken contract address. Defaults to USDC on the chosen network.
schemeDefault "exact". Only exact is supported today (EIP-3009 transferWithAuthorization). upto (Permit2) is planned.
descriptionHuman-readable string shown to the client in the 402 response. x402-SDK-enabled wallets surface this to the user before initiating payment — keep it short and actionable. Example: "Hourly RPC access at $0.001 / request".
maxTimeoutSecondsHow long a payment authorization is valid. Default 300.
rateLimitBudgetPer-payer rate-limit budget. The payer's wallet address is their user ID.
verifyOnlyWhen true, skip the settle step (verify-only). Useful for testing without real charges.
extraFree-form object merged into the 402 response's payment requirements. Used for EIP-712 domain params that your facilitator doesn't supply automatically (e.g. name, version, chainId, verifyingContract for an ERC-20).

extra — EIP-712 domain params for non-USDC assets

When paying with an ERC-20 other than USDC, the facilitator may not supply the EIP-712 domain parameters automatically. Pass them in extra so the wallet can construct a valid transferWithAuthorization signature. The required fields match the token's EIP712Domain struct:

x402:
  facilitatorUrl: https://x402.org/facilitator
  sellerAddress: 0xYourWalletAddress
  pricePerRequest: "1000000000000000"   # 0.001 WETH at 18 decimals
  network: "eip155:8453"                # Base mainnet
  asset: "0x4200000000000000000000000000000000000006"  # WETH on Base
  extra:
    name: "Wrapped Ether"
    version: "1"
    chainId: 8453
    verifyingContract: "0x4200000000000000000000000000000000000006"

See the x402 spec (opens in a new tab) for the full list of payment requirement fields that extra can override.

How the flow works: a client without payment hits eRPC → receives HTTP 402 with payment requirements → signs a payment authorization → retries with the auth attached → eRPC calls the facilitator's verify and (if verifyOnly: false) settle → forwards the request upstream.

Clients using the x402 SDK (opens in a new tab) or Circle Gateway (opens in a new tab) handle the 402 flow transparently. The payer's wallet address becomes their per-payer user ID.

Metrics: erpc_x402_payment_total (counter — verified, settled, rejected), erpc_x402_facilitator_request_total (counter — calls to verify/settle/supported endpoints), and erpc_x402_facilitator_request_duration_seconds (histogram). A bundled Grafana dashboard ("x402 Payments" row) visualizes these.

Method filtering — interaction rules

ignoreMethods and allowMethods work on every strategy. Same rules as upstream-level filters:

  • Both accept matcher syntax (*, |, !).
  • allowMethods takes precedence when both match.
  • Setting allowMethods without ignoreMethods implicitly adds ignoreMethods: ["*"].
  • A strategy that rejects a method due to filtering is treated as "not applicable" — eRPC tries the next strategy.

Example: backend gets all methods, browser frontend only gets read methods.

auth:
  strategies:
    - type: secret
      secret: { value: ${BACKEND_SECRET} }
      # No method filter → backend gets everything
 
    - type: jwt
      jwt:
        verificationKeys: { "kid-1": "${PUBLIC_KEY_PEM}" }
        allowedIssuers: ["https://my-app.example"]
      allowMethods:                # frontend: read-only
        - eth_call
        - eth_blockNumber
        - eth_chainId
        - eth_getLogs
        - eth_getBalance

Rate-limit budget binding

Three layers, applied in order of specificity:

  1. secret.rateLimitBudget (only on secret strategy) — per-token budget
  2. strategies[].rateLimitBudget — per-strategy budget
  3. jwt.rateLimitBudgetClaimName — per-JWT-claim budget (lets the issuer assign tier-based budgets in the token itself)

If multiple match, the most-specific one wins. If none is set, the request is unmetered by auth (but project.rateLimitBudget may still apply).

Combining multiple strategies — full example

projects:
  - id: main
    auth:
      strategies:
        # Backend services — full access, generous budget
        - type: secret
          rateLimitBudget: backend-tier
          secret:
            id: backend
            value: ${BACKEND_SECRET}
 
        # Internal infrastructure — IP-allowlisted, dev-only methods allowed
        - type: network
          rateLimitBudget: internal-tier
          allowMethods: ["*", "debug_*", "trace_*"]
          network:
            allowedCIDRs: ["10.0.0.0/8"]
            allowLocalhost: true
 
        # Frontend users — JWT-gated, read-only RPCs, per-tier budgets via claim
        - type: jwt
          allowMethods: ["eth_call", "eth_blockNumber", "eth_chainId", "eth_getLogs"]
          jwt:
            verificationKeys: { "kid-1": "${JWT_PUBLIC_KEY}" }
            allowedIssuers: ["https://api.my-app.example"]
            rateLimitBudgetClaimName: tier        # client tier read from `tier` claim
 
        # Wallet-signed access for dApp users without our issuer
        - type: siwe
          rateLimitBudget: siwe-tier
          siwe:
            allowedDomains: ["my-dapp.example.com"]

A request is authorized if ANY strategy accepts it. eRPC chooses based on the request shape (token vs. JWT header vs. SIWE params vs. IP).

Common pitfalls

  • Exposing secret on a frontend — anyone can copy it. Use jwt or siwe for browser-originated traffic.
  • Missing trustedProxies behind a load balancer — the X-Forwarded-For chain is taken at face value. Any client can spoof their IP. Always set trustedProxies to your LB / CDN ranges.
  • Forgetting allowedAlgorithms on JWT — without it, the parser accepts every algorithm advertised in the JWT header, opening the door to algorithm-confusion attacks (e.g. an attacker switching to HS256 with the public key as HMAC secret).
  • rateLimitBudget set but no budget defined — the request is auth-only, no rate-limit applied. Add the budget under top-level rateLimiters.budgets[].
  • siwe.allowedDomains empty — every domain is accepted, so a SIWE message signed for a phishing domain would authenticate the user against you.
  • x402.verifyOnly: true in production — no settlement happens; you've shipped a free RPC.
  • JWT rateLimitBudgetClaimName typo — the claim is silently absent, so the per-user budget isn't applied. Verify with a test JWT.

Append .llms.txt to this URL (or use the AI link above) to fetch the entire expanded reference as plain markdown for an AI assistant.