Authentication
AIOpen as plain markdown for AIEach 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 token ⇒ secret, an Authorization: Bearer … header ⇒ jwt, etc.).
Supported strategies:
secret— static API key (backend-to-backend)network— IP / CIDR allowlistjwt— JSON Web Token, public-key verified (recommended for frontends)siwe— Sign-in-with-Ethereum signed messagex402— 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 authorizesrateLimitBudget— 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.
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 worksThe 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 eRPC | secret (use env vars for the value) |
| Lock down to specific datacenters / VPNs / public IPs | network |
| Frontend dApps with per-user rate limits and expirations | jwt |
| Wallet-signed access without running an issuer | siwe |
| Charge per request via stablecoin | x402 |
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
| Field | Type | Notes |
|---|---|---|
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
| Field | Type | Notes |
|---|---|---|
type | "secret"|"network"|"jwt"|"siwe"|"x402"|"database" | Required. Selects the strategy. database is managed via the Admin API — not configurable via YAML/TS. |
ignoreMethods | string[] | Block these methods (matcher syntax: *, |, !). |
allowMethods | string[] | Allowlist; when set, blocks everything not listed. When allowMethods is set and ignoreMethods is not, ignoreMethods: ["*"] is implicit. |
rateLimitBudget | string | Bind requests authorized by this strategy to a budget defined under top-level rateLimiters.budgets[]. |
| Strategy-specific block | object | One 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| Field | Notes |
|---|---|
secret.id | Optional label. Used as a metric label so multiple secret strategies can be told apart in dashboards. |
secret.value | The static token. Required. Use env-var interpolation (${VAR} in YAML, process.env.VAR in TS) to keep secrets out of the config. |
secret.rateLimitBudget | Per-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| Field | Notes |
|---|---|
allowLocalhost | When 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. |
ipAsUser | When true, the client's IP becomes their per-user identifier for rate limiting — rateLimitBudget then bills per IP rather than across the whole strategy. |
rateLimitBudget | Per-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"| Field | Notes |
|---|---|
verificationKeys | Map 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). |
rateLimitBudgetClaimName | The 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| Field | Notes |
|---|---|
allowedDomains[] | List of domains the SIWE message's Domain field must match. |
rateLimitBudget | Per-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"| Field | Required | Notes |
|---|---|---|
facilitatorUrl | ✅ | x402 facilitator endpoint that handles verify and settle ops. |
sellerAddress | ✅ | Wallet address that receives payments. |
pricePerRequest | ✅ | Cost per request in atomic units of asset. For USDC (6 decimals), "1" = $0.000001. |
network | ✅ | x402 network identifier — eip155:8453 for Base mainnet, eip155:84532 for Base Sepolia. |
asset | Token contract address. Defaults to USDC on the chosen network. | |
scheme | Default "exact". Only exact is supported today (EIP-3009 transferWithAuthorization). upto (Permit2) is planned. | |
description | Human-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". | |
maxTimeoutSeconds | How long a payment authorization is valid. Default 300. | |
rateLimitBudget | Per-payer rate-limit budget. The payer's wallet address is their user ID. | |
verifyOnly | When true, skip the settle step (verify-only). Useful for testing without real charges. | |
extra | Free-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 (
*,|,!). allowMethodstakes precedence when both match.- Setting
allowMethodswithoutignoreMethodsimplicitly addsignoreMethods: ["*"]. - 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_getBalanceRate-limit budget binding
Three layers, applied in order of specificity:
secret.rateLimitBudget(only onsecretstrategy) — per-token budgetstrategies[].rateLimitBudget— per-strategy budgetjwt.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
secreton a frontend — anyone can copy it. Usejwtorsiwefor browser-originated traffic. - Missing
trustedProxiesbehind a load balancer — the X-Forwarded-For chain is taken at face value. Any client can spoof their IP. Always settrustedProxiesto your LB / CDN ranges. - Forgetting
allowedAlgorithmson 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). rateLimitBudgetset but no budget defined — the request is auth-only, no rate-limit applied. Add the budget under top-levelrateLimiters.budgets[].siwe.allowedDomainsempty — every domain is accepted, so a SIWE message signed for a phishing domain would authenticate the user against you.x402.verifyOnly: truein production — no settlement happens; you've shipped a free RPC.- JWT
rateLimitBudgetClaimNametypo — 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.