Configuration
The authoritative field table is SPEC §4.3, and every field in it is implemented. This page is not a second copy of that table; it is the set of fields whose behaviour is not obvious from a one-line description, written out in full so the reasoning is not rediscovered — or quietly re-invented — later.
Read the API reference for signatures and the policy guide for how the caps compose.
routing.maxQuoteAgeMs — conditional, and inert for standard v2 challenges
Section titled “routing.maxQuoteAgeMs — conditional, and inert for standard v2 challenges”Default: 5000 · SPEC §4.3, §6.3 step 12 · ADR-010 decision 3
SPEC describes this as “Reject older PaymentRequired timestamps when present”, and
SPEC §6.3 step 12 likewise says “when defined by protocol”.
Upstream x402 v2
PaymentRequiredcarries no timestamp.
The verified shape at @x402/core 2.20.0 is:
type PaymentRequired = { x402Version: number; error?: string; resource: ResourceInfo; // { url, description?, mimeType?, serviceName?, tags?, iconUrl? } accepts: PaymentRequirements[]; extensions?: Record<string, unknown>;};There is no issuedAt, no timestamp, and no expiresAt. The only place a challenge
timestamp can appear is inside a requirement’s scheme-specific extra object.
So the check is implemented, and it is conditional: it looks for a timestamp in extra,
and where none is present — which is every standard v2 challenge — it does nothing.
What this means in practice. The default of 5000 is not an active protection against
stale quotes. Setting it lower does not tighten anything, and the field being non-zero should
not be read as evidence that challenge freshness is being enforced. What does bound the
window is the authorization lifetime: min(60s, maxTimeoutSeconds), never exceeding the
merchant’s own bound (SPEC §6.6).
The field is kept rather than removed for two reasons: SPEC §4.3 defines it, and a scheme
that starts putting a timestamp in extra gets the check for free.
routing.rpcOverrides — your endpoint instead of the manifest’s
Section titled “routing.rpcOverrides — your endpoint instead of the manifest’s”ADR-015
The signed manifest ships keyless public RPC endpoints, because those are the only ones that can be published to every installation. A keyless public endpoint has a per-IP quota, and at any volume you will hit it.
routing.rpcOverrides replaces the endpoint list for one network, and changes nothing else:
const tx402 = createTx402Client({ signers: { solana }, routing: { rpcOverrides: { "solana:devnet": ["https://your-provider.example/v2/<key>"], }, },});Tx402Client( solana_signer=solana, routing=RoutingPolicy( rpc_overrides={"solana:devnet": ["https://your-provider.example/v2/<key>"]}, ),)Validated at construction, so a mistake is an error rather than a setting that quietly never applies:
| Rule | Why |
|---|---|
| The key is resolved through the manifest | An unknown or misspelled network fails immediately. An override that never matches would leave you believing your keyed endpoint is in use while every read still goes to the public one. |
| An empty list is rejected | “Override with nothing” is a mistake, not a request to fall back. |
https: only, except on localhost |
An RPC endpoint usually carries its API key in the path or query. http: is allowed on localhost, 127.0.0.1, and [::1] for a local validator. |
| Nothing else is overridable | Which networks exist, which assets they carry, and a token’s decimals still come from the signed document. |
tx402 never reads the environment for this, or for anything else. If you keep the URL in an environment variable, your code passes it in.
policy.allowedNetworks and routing.preferNetworks — aliases in, canonical out
Section titled “policy.allowedNetworks and routing.preferNetworks — aliases in, canonical out”SPEC §4.1, §4.3, §7.2 · ADR-010 decision 4
SPEC §4.1’s example writes "solana:mainnet". Upstream never emits that. Solana CAIP-2
identifiers are genesis-hash based:
| Cluster | Canonical CAIP-2 | Alias |
|---|---|---|
| Mainnet | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp |
solana:mainnet |
| Devnet | solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 |
solana:devnet |
Configuration accepts either form. The alias map lives in the signed release manifest, so it is as tamper-evident as the network list itself, and a manifest whose alias shadows a real network is rejected at construction.
Everything downstream — policy matching, route selection, health indexing, diagnostics — keys on the canonical identifier. The alias is display and input only.
This is a correctness rule rather than cosmetics: keying health or policy on an alias would silently fail to match a merchant’s canonical offer, and the failure would look like “the merchant does not support Solana” rather than like a bug.
An identifier that is neither a declared network nor a declared alias is a ConfigurationError
at construction, not a value passed through unresolved. Passing it through would let policy
accept a network the SDK cannot pay on.
manifest — verified at construction, offline, against compiled-in keys
Section titled “manifest — verified at construction, offline, against compiled-in keys”SPEC §4.3, §5.4 · ADR-012
Defaults to the signed manifest bundled with the build. A caller-supplied manifest is verified on identical terms; there is no “trust me” mode.
Verification is offline and synchronous, and failure prevents construction — it is never
downgraded to a warning, because everything downstream treats manifest contents as
authoritative. Rejection reasons are stable identifiers (expired, signature-mismatch,
unknown-key-id, …) reported in the error’s details.reason.
Two consequences worth knowing before S3:
- Expiry is real. The bundled manifest stops verifying on 2027-08-02, at which point client construction fails until it is re-issued. See the manifest runbook.
requiredNetworksis not applied by default. SPEC §5.4’s four-network requirement binds the bundled manifest, which a test asserts directly. A caller-supplied manifest may legitimately declare a single network — a local integration fixture, for instance — so verification requires nothing unless asked.
signers — abstractions only (SEC-001)
Section titled “signers — abstractions only (SEC-001)”signers.evm takes anything satisfying SPEC §7.1’s EvmSigner: a kind, an async
getAddress(), and signTypedData(request). The core client never accepts a private key, and
there is no environment-variable fallback — SPEC §15 forbids silently substituting an
environment key for a configured signer.
The address is resolved on first use and cached per signer object, not at construction:
createTx402Client validates synchronously (SPEC §4.1) and cannot await an async lookup. A failed
lookup is not cached, so a transient KMS outage does not disable a signer for the life of the
process. ADR-010 decision 5a records this.
Chain adapters load lazily. signers.evm alone is enough — importing tx402/evm by hand is only
necessary to build a signer or inspect a plan. A configured signer for a family whose adapter does
not exist yet (Solana, until M4) produces UnsupportedSchemeError listing the networks that were
offered, never a silent skip.
The convenience adapter for a raw key lives behind its own import:
import { privateKeyToEvmSigner } from "tx402/signers";It exists for development and for dedicated low-balance wallets. Prefer a KMS, a hardware wallet, or a remote signing service — SPEC §9.1 lists prompt injection extracting a wallet key as a live threat for the agent runtimes this SDK targets, and a key in process memory is a key an in-process compromise can read.
timeouts — the caller’s own deadline is never shortened
Section titled “timeouts — the caller’s own deadline is never shortened”| Field | Default | Behaviour |
|---|---|---|
timeouts.initialRequestMs |
absent | No SDK deadline. The caller’s transport or AbortSignal governs. Supplying one adds a deadline alongside any caller signal, never replacing it. |
timeouts.paymentRetryMs |
10000, minimum 1000 |
Covers the signature-bearing attempt. |
A paid retry that hits its deadline is ambiguous, not failed: the signature was already
transmitted, so AmbiguousPaymentError is raised and the reservation is retained until its
120-second TTL (SPEC §6.7). Setting this very low does not make failures cleaner — it makes
ambiguous outcomes more likely.
disableRequestIdHeader
Section titled “disableRequestIdHeader”Omits X-TX402-REQUEST-ID from the paid retry. The header carries a UUIDv7 and no payment meaning
(SPEC §6.7); turn it off for merchants that reject unknown headers. The caller’s own
Idempotency-Key is always preserved and is never synthesized — merchant semantics are unknown, so
inventing one would be guessing.
Every remaining field
Section titled “Every remaining field”Implemented with exactly the semantics SPEC §4.3 states, and listed here so this page is a complete reference rather than a selective one.
| Field | Default | Behaviour |
|---|---|---|
signers.evm |
absent | Required to select an EVM route. A two-method interface, so a KMS or hardware signer is first-class. |
signers.solana |
absent | Required to select a Solana route. |
policy.maxPerRequest |
"0.50 USDC" |
Per-payment ceiling. Integer atomic units; a decimal that does not divide exactly into the asset’s atomic unit is rejected. |
policy.maxPerHour |
"10.00 USDC" |
Rolling 60-minute cap over committed entries plus live reservations, per scope and asset. Must be ≥ maxPerRequest. |
policy.allowedDomains |
["*"] |
Matched against the normalized host before the first request and before the paid retry. |
policy.allowedNetworks |
Base + Solana production | Empty list is invalid. Aliases accepted in, canonical CAIP-2 out — see above. |
policy.maxPaidAttempts |
2 |
Range 1–3. Counts signed retries only, never the initial unpaid request. Exhaustion is a typed terminal error, not a loop that stops. |
timeouts.initialRequestMs |
caller’s own | Absent by default: the SDK never silently shortens a caller’s timeout. |
timeouts.paymentRetryMs |
10000 |
Covers the one signature-bearing request. Minimum 1000. |
routing.preferNetworks |
[] |
Implemented. A tie-break preference only — it cannot make a non-viable route viable, and it ranks below viability. See SPEC §6.4 step 18. |
routing.rpcOverrides |
{} |
Caller-supplied RPC endpoints per network, for keyed or private providers (ADR-015). |
spendStore |
MemorySpendStore |
The pluggable ledger. Its scope key is the normalized merchant host, so two processes sharing a store share a cap (ADR-018). |
logger |
no-op | Receives redacted structured events (SPEC §10). The SDK never writes to the console itself. |
clock |
system + monotonic | Injectable for tests only. |
manifest |
bundled signed manifest | Signature and expiry verified synchronously at construction; a failure is a construction failure, not a warning. |
allowInsecureLocalhost |
false |
Permits http:// to localhost. For a local test merchant, and nothing else. |
Solana is implemented
Section titled “Solana is implemented”An earlier revision of this page called Solana and routing.preferNetworks future work.
Both shipped: Solana at M4 with SPL exact transfers and pre-sign transaction validation, and
the deterministic route planner that consumes preferNetworks at M5. The S15 audit filed
the stale text as O50.