The standard x402 door.
Pay for a check with a stock x402 v2 client at POST /v1/x402/checks. This page is the published contract: what a client must do, how to configure the official SDKs, the timeout and retry rules, the private recovery record, and the optional Sign-In-With-X attestation. The original door (/v1/checks) remains supported and unchanged.
The contract.
Any buyer — any wallet, SDK, agent platform or hand-rolled signer — can purchase through POST /v1/x402/checks (canonical URL https://api.secondedoracle.xyz/v1/x402/checks; a GET returns 405) when it meets all five conditions. Nothing keys on a brand or User-Agent; named SDKs are test cases, not a gate.
x402 v2
Speak x402 v2: read the base64 JSON
PAYMENT-REQUIREDheader (identical to the 402 body) and send exactly onePAYMENT-SIGNATURE. A v1X-PAYMENTpresentation is refused 400x402_version_unsupportedbefore anything is charged.Exact EIP-3009
Use scheme
exactwith EIP-3009transferWithAuthorization. Permit2,uptoandextra.assetTransferMethodare not offered.Echo
acceptedEcho the selected
accepts[i]unchanged asaccepted, including its nonexclusive public terms ticket. The stock SDKs do this by default.A supported cell
Pay from a supported and enabled wallet, token and network cell — an ordinary (EOA) wallet with USDC on Base or Arc, or USDG on Robinhood Chain. See Networks and wallets.
Present promptly
Present within the quote lifetime and the remaining-validity bounds in the table below. Sign promptly with a synchronized clock; a stock SDK meets this when it signs right after the 402 with a clock within 30 seconds of UTC.
The public offer is a nonexclusive terms template: each verified payer selects a separate private purchase, and at most one network quote can be admitted for that purchase. The challenge is reconstructed from stored immutable offer content and quote rows, including resource, Bazaar, SIWX declaration, token metadata, timeout and all accepted terms. Later releases, configuration changes and ticket-key rotation cannot change these original terms. Public 402 bodies, PAYMENT-REQUIRED headers and SIWX claim responses contain public pricing, domain and validity terms plus opaque tickets; they contain no check, quote or offer IDs, salt, request commitment or exact billable byte count. Public prices still reveal the price tier. Recovery compares the stored terms, full original authorization and request binding without requiring a retired ticket key. A second authorization nonce for the same template and payer conflicts; an identical credential recovers the original admission before expiry and new-sale gates. Signatures that fail cryptographic verification leave durable payment state unchanged.
The door is enabled per cell — a network, token and wallet type. A cell that is not enabled is left out of new 402s, and a payment for it is refused cleanly before any chain read, with nothing charged. While the whole door is disabled, new purchases are refused 503 door_disabled; recovery of an already-admitted purchase keeps working. The API describes both doors at api.secondedoracle.xyz/v1/openapi.json, and the agent-readable summary of this page is secondedoracle.xyz/skill.md.
Pinned SDKs and configuration.
These are the tested protocol versions — evidence that the contract is honest, not a promise about arbitrary client brands. A hand-rolled signer meeting the same contract also pays; the interoperability suite runs one built on viem.
Python x402-2.24.0-py3-none-any.whl | 515171258b32af36c05b2a6aa8d2e86ff6cf70ecc731151562fdd90859987fe1 |
|---|---|
TypeScript @x402/core@2.27.0 | 7b5a17f544b907ee875dcbca57f50cce71369a5c26d06527fad6b0ec54aa52db |
@x402/evm@2.27.0 | ff8fda24235035a10d6e02a3f808a1c2b7ebabcfb37a3125f484d2494ab36901 |
@x402/fetch@2.27.0 | 14179d041e5c37ef101aae26db119a4450871233f5bee459b7b7482189321fcf |
@x402/extensions@2.27.0 | f81b15baa890b33f045ca09508261447c3c0ca1c55ec66c53391006feeb4a785 |
Register the exact EVM scheme on every network you intend to use. Raise the default $1 cap to the quoted price (prices reach $2.50); keep spend controls enabled — disabling them is not a supported configuration. Arc USDC and Robinhood USDG need explicit allowed-asset entries with integer atomic caps. The default selector chooses the first eligible offer without checking your balance, so send options.network for the chain on which you are funded.
Python configuration requires Python ≥ 3.10 for x402==2.24.0; pip on Python 3.9 reports no matching versions. Use Python 3.10 or newer for both installation and execution. (account is eth_account.Account.from_key(...) for a dedicated wallet; no private key belongs in this example). The per-purchase cap is set to exactly the quoted price, read from a free unpaid 402 — not to the $2.50 maximum. For a Small quote, that is $0.25 at the dollar level and 250000 atomic at the asset level:
# pip install "x402[evm,httpx]==2.24.0" rfc8785 cryptography
from decimal import Decimal
import httpx
from x402 import x402Client
from x402.mechanisms.evm.exact import ExactEvmScheme
from x402.mechanisms.evm.signers import EthAccountSigner
from x402.http.clients.httpx import x402AsyncTransport
import recover # recover.py from this page, saved next to your script
body = {"product": "scam_check", "input": {"message": "Suspicious message to inspect"},
"options": {"network": "eip155:8453"}}
url = "https://api.secondedoracle.xyz/v1/x402/checks"
# A free unpaid POST returns the 402 terms. Pin the payee, then quote the price.
challenge = httpx.post(url, json=body).json()
quoted = [a for a in challenge["accepts"] if a["network"] == body["options"]["network"]][0]
assert quoted["payTo"].lower() == "0x010ab46d566cde25cca0ee55eb105e781c7bcf3a"
client = x402Client()
client.register(quoted["network"], ExactEvmScheme(signer=EthAccountSigner(account)))
client.set_spend_controls({
"max_amount_per_payment": f"${Decimal(quoted['amount']) / 1_000_000:.2f}", # the quoted price
"allowed_assets": [{"network": quoted["network"], "asset": quoted["asset"],
"max_amount_per_payment": quoted["amount"]}], # integer atomic cap
})
recover.install_hooks(client, purchase_file, url, body)
# ONE purchase through the wrapper; polling and recovery use a PLAIN client.
async with httpx.AsyncClient(transport=x402AsyncTransport(client)) as session:
reply = await session.post(url, json=body) # 202 = running; poll via recover.recover()The complete runnable buyer — terms pinning, per-purchase caps, the recovery record, 202 polling with the same credential, receipt verification and answer interpretation, including the terminal no-charge ending — is buy.py, exercised end to end against the door’s interoperability fixture. Its --max-price defaults to 0.25 (Small); a larger cap is used only when you pass it explicitly.
TypeScript/Node, the same flow sketched (recover.mjs exports ESM functions importable from TypeScript):
import { x402Client } from "@x402/core/client";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
import { installHooks, loadRecord, recover, archiveTerminal } from "./recover.mjs";
const body = { product: "scam_check", input: { message: "Suspicious message to inspect" },
options: { network: "eip155:8453" } };
const url = "https://api.secondedoracle.xyz/v1/x402/checks";
// A free unpaid POST returns the 402 terms. Pin the payee, then quote the price.
const challenge = await (await fetch(url, { method: "POST",
headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })).json();
const quoted = challenge.accepts.find(a => a.network === body.options.network);
const client = new x402Client();
registerExactEvmScheme(client, { signer: account, networks: [quoted.network] });
client.setSpendControls({
maxAmountPerPayment: `$${(Number(quoted.amount) / 1e6).toFixed(2)}`, // the quoted price
allowedAssets: [{ network: quoted.network, asset: quoted.asset,
maxAmountPerPayment: quoted.amount }], // integer atomic cap
});
await installHooks(client, purchaseFile, url, body);
// 1. ONE purchase via wrapFetchWithPayment, then drop the wrapper.
// 2. A 202 is pending: poll with recover(loadRecord(purchaseFile), keys) —
// the same credential over plain fetch, never a second signature.
// 3. recover() verifies the receipt. An envelope with an answer is the result:
// interpret label_id via answer_to_action, then read coverage. A terminal
// no-charge receipt (state service_failed, charged "no") means stop:
// archiveTerminal(purchaseFile, reply); a new purchase later is allowed
// only when hints.new_quote_allowed is true.Timeout and retry guide.
Re-POST the exact same credential and body when uncertain. Never ask an automatic payment wrapper to recover, and never sign a second payment for the same purchase. Stock SDKs return a 202 without polling. A 402 to a paid retry can cause another signature, so this door never returns 402 to a paid request. Any purchase can outlast its first POST, on every network. The wrapper hazard is concrete in the pinned SDKs: in Python x402 2.24.0, an on_payment_response hook that returns a recovery result makes x402AsyncTransport (x402/http/clients/httpx.py) create a fresh payment payload and send a second signed payment for the same purchase. recover.py’s before-creation hook refuses while a record exists, which blocks that path — but never point the wrapper at recovery yourself.
| Quote lifetime W | 120 |
|---|---|
| Clock skew S | 30 |
| Minimum remaining R | 260 |
| maxTimeoutSeconds T = R + W | 380 |
| Maximum remaining T + S | 410 |
| max_valid_before offset from issued_at | 530 |
| SIWX closes_at offset from offer expiry | 150 |
| SIWX maximum issuedAt age | 120 |
| SIWX maximum future issuedAt | 30 |
Honour Retry-After and hints.poll_after_s; Prefer: wait=N is capped at 25 seconds. At most one poll may be in flight per check at a time, and at most eight in flight per payer; a poll that returns frees its slot, so polling one check several times in sequence is fine. The Python helper allows up to 30 minutes and 1800 attempts by default; Node allows 300 seconds and 20 attempts. Each request is limited to 35 seconds or the remaining deadline. Accept numeric or HTTP-date Retry-After and wait the maximum of it, hints.poll_after_s, and one second. Stop before a wait reaches the deadline and never sleep after the last attempt. A bounded polling budget ending means retain the record and resume later, never re-sign. Recovery of an admitted purchase runs before every new-purchase gate, and quote or authorization expiry does not end it. Quote expiry does not end recovery of an admitted check. Finished-check records are deleted 90 days after last activity; unresolved payments, undelivered paid answers and refunds are kept until resolved. Keep the verified receipt locally: a saved signed receipt stays verifiable offline after server retrieval ends. Privacy retention eventually returns a non-402 refusal with do_not_resign: true (unknown or removed offers use offer_not_found).
Terminal service failure: verified, and nothing charged. A reply whose signed receipt says state: service_failed with billing.charged: "no" is final — the live door returns it as a 503; the helpers recognise verified terminal no-charge receipts on HTTP 200 or 503. Do not retry the credential: the check failed on our side and polling cannot change it. Verify the receipt, archive the record, and — only when hints.new_quote_allowed is true — start a deliberate new purchase later, from a fresh 402 with a new record file. The receipt’s reason names the cause: prefetch_unavailable, a data source the check needed (a chain reader, an official token list, public records) could not be read in time; provider_unavailable, a model provider could not be reached; engine_error, an internal failure on our side.
Measured answer times, typical and not a promise. In the door’s pre-launch trials on the test networks, with the pinned SDKs and a deterministic model fixture, purchase start through verified answer took about 30–41 seconds on Arc testnet, about 1–2.5 minutes on Base Sepolia, and about 2.5–3.2 minutes on Robinhood Chain testnet; recovering an already-paid Robinhood purchase took 17 seconds. Every purchase first returned 202, and the buyer polled or recovered. These are transport and payment timings on test networks — Arc releases the answer at its safe tag, Base and Robinhood release at soft inclusion depth — not production model latency and not an SLA. On the mainnets, expect typically tens of seconds on Arc, usually under 2 minutes on Base, and one to two minutes on Robinhood Chain. Finality and capacity delays may extend any purchase beyond its initial request. Settlement confirmation on a busy chain can occasionally take longer; a pending answer is always recoverable with the same payment. Keep the payment record and resume recovery later. Do not pay again.
Classify the HTTP status and response shape before reading receipt. The recovery helpers accept an answer only from HTTP 200 with a verified receipt, an included, final or released state, and a nonempty answer object. A signed 202 must verify and have a pending state (running, settling, delayed or unavailable), no answer and no PAYMENT-RESPONSE header; a receipt-free 202 is unverified pending information.
A plain-text ingress 503 is retryable. Structured 503 errors store_unavailable, internal_error and verification_unavailable are retryable only with do_not_resign: true and new_quote_allowed: false. A retry proves neither payment nor answer entitlement. Other refusals stop recovery and preserve the private record. Snapshot and resend the exact same destination, body and PAYMENT-SIGNATURE through a plain HTTP client; never re-sign or follow redirects.
Private recovery record.
Write one private, durable recovery record per purchase, from the SDK’s after-creation hook, before the first paid send. It is the only thing, with the trusted public /v1/keys data, that a fresh process needs to recover and verify a purchase.
url, body | Application URL, checked against resource.url, and the exact original request |
|---|---|
payment_signature | SDK encoder output; reused byte-for-byte for polling |
accepted | The entire selected requirement, including its public terms ticket |
quote | accepted.extra.seconded: public tier, schema/predicate versions and deadlines |
canonical_request | RFC 8785 construction from the body and original quoted terms, described under Receipts in the docs |
format, purchase_association | seconded-door-recovery/v2 and the locally computed association to compare with the signed receipt |
state | unresolved until a verified terminal outcome; do not overwrite |
payment_signature_sha256 | Local corruption and swap detection for the retained header; not an independent server attestation |
Use recover.py or recover.mjs, one client and one new record file per purchase. Create a user-owned directory with mode 0700 first. The before-creation hook refuses an existing file; the after-creation hook writes a private 0600 temporary file, fsyncs it, atomically renames it, then fsyncs the directory. An exclusive lock prevents concurrent writers. A write failure raises before any paid send. Existing records are never overwritten, even after resolution: archive them deliberately and select a new file for a new purchase. A crash can leave a lock; inspect it and recover before any new signature. An in-memory alternative provides process-lifetime recovery only.
A fresh process needs only that file and trusted public /v1/keys data:
python recover.py private/purchase.json public-keys.json
node recover.mjs private/purchase.json public-keys.jsonBoth examples verify these checks before accepting a recovered answer:
Receipt Ed25519 signature, using its named public key and versioned signing prefix.
The receipt’s signed
purchase_associationequals the association recomputed from the exact original authorization, accepted terms and canonical request. This authenticated association locates the private check ID.New records require this association and fail closed if it is missing. Retained records keep their original salted commitment and ID checks for historical receipts. Pre-upgrade offers paid with their original exact accepted terms keep the original check ID and commitment, so already-distributed recovery examples can verify them after upgrade.
Receipt billing matches the retained authorization and accepted network, asset, amount, payer and payee. Missing or corrupt fields produce
unverified.Optionally, public chain evidence: a matching AuthorizationUsed(from, nonce) and Transfer in
billing.tx. The examples expose a chain-check callback; no RPC is performed by default.
The association block is {"v":1,"sha256":"<64 lowercase hex>"}. Its digest is SHA-256 of seconded-purchase-association/v1 followed by a NUL byte and RFC 8785 JSON of {"authorization":A,"accepted":T,"request":R}. A contains the complete EIP-3009 from, to, value, validAfter, validBefore and nonce; addresses and nonce are lowercase and integers are decimal strings. T is the exact selected accepted requirement with only extra.ticket removed; it binds scheme, network, asset, token domain, price and public validity terms. R is the canonical request object constructed above. The association binds the input but does not hide it from a receipt holder who can guess it: authorization fields become public on settlement. No standalone input hash appears in a public 402. The block is covered by the receipt's existing Ed25519 signing prefix for versions 1–3. Historical receipts omit the block and retain their original signatures. Their ID, salted commitment and billing checks cannot retroactively attest authorization nonce or validity fields that were absent from the signed receipt. Recovery examples validate canonical unsigned decimal authorization values; use strings for uint256 fields, especially values beyond JavaScript’s safe integer range.
The examples preserve the record on every error, timeout and pending outcome; on a verified terminal no-charge receipt (state: service_failed and friends, charged: "no") they print a clear no-charge result and archive the record with the terminal reply beside it, instead of resuming forever. After verifying and storing the answer, remove the credential under your retention policy. After a no-charge refusal, keep it private until its validBefore has passed. Only a verified refusal saying charged: "no" with new_quote_allowed: true permits a deliberate new purchase; a pending, unverified or transport error never does. The legacy payer ownership GET is a second recovery path.
The residual.
On the standard door, until your payment is first admitted, SECONDED relies on your payment header staying private. Anyone who captures it before then could use your payment for their own check. The door admits a payment only while its authorization has 260 to 410 seconds of validity left. With the official SDKs and a clock within 30 seconds of UTC, that ends at most about two and a half minutes after you sign; a signer that sets its own expiry can stay usable for longer. The optional x402 Sign-In-With-X extension adds protection only to a purchase whose claim the door answers as recorded, paid from that same wallet, under the conditions at Optional SIWX. If your wallet declines to sign, the purchase goes ahead without that protection; if the door refuses the claim, nothing is paid. A refused payment stays valid on-chain until it expires, so keep it private until then. Never log, share or proxy-capture payment headers or recovery records; treat them like passwords. The original door (/v1/checks) binds your signature to the quote and does not have this gap.
The choice between doors is simple: an agent using a stock x402 SDK uses this standard door; a custom client that can sign with the nonce the 402 supplies may use either, and on the original door gains the payer-signed nonce binding that closes this gap.
Optional SIWX.
SIWX is optional payer attestation, not an EIP-3009 cryptographic binding to this request. Protection applies only if the door replies recorded, the payment comes from that same wallet, the exact original offer and accepted terms are used, and the claim is still live. The public nonce resolves terms: each valid SIWX signature creates or selects only its signer’s claim and purchase; stock SIWX claims do not carry a ticket. A successful claim returns the original immutable challenge; missing retained rendering data/key returns 503 claim_unavailable, no new durable claim and no payment. On payer_claim_pending, serialize purchases and wait for retry_after; a refused claim stops payment. A wallet declining SIWX continues without protection. Stock wrappers pay using the first challenge after a 402 claim reply, so do not infer protection from merely enabling an extension. One wallet runs one purchase handshake at a time.
Protection additionally requires the claim to be recorded before the authorization exists, and validBefore < closes_at + R. Equality is outside this condition: at now = closes_at, an authorization with validBefore = closes_at + R remains admissible after the claim closes. With the pinned SDK expiry construction and a synchronized clock, an authorization signed before offer expiry satisfies this condition; a custom expiry can outlast it.
To protect the intended purchase, no other claimed offer of that payer may be pending while the authorization remains admissible. Do not abandon an unadmitted purchase and start another at the same price/network: the leftover authorization could buy the payer's later offer. Claims persist through admission, voiding, refusals and restart until closes_at. A captured whole paid request can still read the buyer's answer. An observer of a public nonce can create only their own claim, and cannot reserve, void, consume or read a different payer’s purchase. Mixing SIWX and non-SIWX purchases from the same wallet can cause refusals until claims close. SIWX is not a recovery or previously-paid-access mechanism.
For hand-rolled EOA signers: sign the EIP-4361 text reconstructed from the exact strings in the transmitted SIWX payload, using EIP-191 personal-message signing. Copy the declaration’s info fields and add address, CAIP-2 chainId (for example eip155:8453), type: "eip191" and the resulting signature. Preserve timestamp spelling: 2026-09-28T00:00:00Z and 2026-09-28T00:00:00.000Z denote the same instant but produce different signed bytes; do not pass timestamps through Date or another formatter unless the transmitted payload uses exactly the same resulting strings. Whole-second validity checks do not normalize the text used for signature recovery.
The message uses LF (\n) separators, no trailing newline, and this field order:
{domain} wants you to sign in with your Ethereum account:
{address}
{statement}
URI: {uri}
Version: {version}
Chain ID: {decimal chain number without eip155:}
Nonce: {nonce}
Issued At: {issuedAt}
Expiration Time: {expirationTime}
Not Before: {notBefore}
Request ID: {requestId}
Resources:
- {resources[0]}Omit Not Before when absent; include Expiration Time for this door’s declaration. Render every resource in its original order on its own - line. Preserve address case, statement, URI and other string contents. Encode the signed payload as base64 JSON in SIGN-IN-WITH-X. After a successful claim’s 402, compare accepted terms by JSON structure and values, ignoring object key order (including nested objects); array order and every quoted value remain significant, and a changed ticket, price, domain, deadline or other public term must stop payment. JSON stringification alone is not an equality check, because storage reconstruction can reorder object keys.
Networks and wallets.
Payment cells on the live service, and the threshold at which each network releases the answer. included is not final; a later rollback never asks the buyer to pay again. Test networks are refused as payment networks.
Base eip155:8453 · USDC | Releases the answer at soft inclusion depth, with monitoring to finality afterwards |
|---|---|
Arc eip155:5042 · USDC | Releases the answer at Arc’s safe (validated committed) inclusion |
Robinhood Chain eip155:4663 · USDG | Releases the answer at soft inclusion depth, with monitoring to finality afterwards |
Smart wallets are not yet supported. A payment from a deployed smart-contract wallet (ERC-1271), an EIP-7702-delegated EOA, or a counterfactual (ERC-6492) wallet is refused 400 unsupported_signature_type before anything is charged, until its cell is separately qualified. x402 v1, Permit2, upto, Solana and Polygon are not offered. A client that does not echo accepted.extra is refused ticket_required, with nothing charged.
Header handling and success.
Never log, share, put in URLs, or proxy-capture payment headers, SIWX signatures or recovery records. Public 402 bodies and their opaque nonexclusive tickets carry no input binding or purchase identifiers. Public terms tickets authorize no access and cannot reserve another payer’s purchase.
Success requires an answer, verified receipt association, and interpretation of the label, recommended action and signed coverage. An unpaid, refused, failed, pending or NOT VERIFIED result is not a pass and must not unblock the guarded action. An HTTP 200 or a transaction hash alone is not success. Without a payer, stop and ask your owner.