@nerochain/x402-extensions
Optional protocol extensions — payment-identifier (idempotency) and access-token (signed proof-of-payment for cached access).
@nerochain/x402-extensions ships optional capabilities that ride on the V2 envelope's extensions field or on orthogonal HTTP headers. They are composable with the canonical @nerochain/x402-server middleware and @nerochain/x402-client signers; nothing in the core wire format changes.
Two extensions are included today.
Install
pnpm add @nerochain/x402-extensionspayment-identifier — idempotency on retries
A client-supplied identifier carried inside extensions.paymentIdentifier. Two requests with the same identifier from the same payer SHOULD result in at most one on-chain settlement; the second response either matches the first (cached) or surfaces a definitive error if the first has not yet completed.
The identifier is opaque to the protocol — any string works. UUIDv4 (or any ≥ 16-byte random value) makes collision negligible.
Client side
import {
generatePaymentIdentifier,
withPaymentIdentifier,
} from "@nerochain/x402-extensions";
const id = generatePaymentIdentifier(); // crypto.randomUUID() under the hood
// Wrap your signer's payload before sending.
const payload = withPaymentIdentifier(originalPayload, id);Reuse the same id across retries of the same logical request. A new id per logical request, a stable id per retry. Persist it in your retry buffer; do not regenerate.
Server / facilitator side
import { extractPaymentIdentifier } from "@nerochain/x402-extensions";
const id = extractPaymentIdentifier(payload);
if (id) {
const cached = await idempotencyStore.get(id);
if (cached) return cached; // dedup hit
// ... process normally, then store the result keyed on `id` ...
}The merchant or facilitator decides where the idempotency store lives. Postgres, Redis, in-memory — any persistence with a Map<string, Result> interface works. The identifier travels through the V2 wire format unchanged.
Relationship to requestHash
requestHash (defined in the aa-native scheme) is the on-chain replay key, derived deterministically from (merchant, chainId, method, endpoint, timestampBucket, clientNonce). It is the authoritative dedup mechanism — the settlement contract rejects duplicate requestHash values regardless of any off-chain extension.
paymentIdentifier is complementary. It lives at the HTTP layer, not the chain layer. It lets a merchant's HTTP layer dedup retries before any chain interaction even happens. Use both: paymentIdentifier saves bundler round-trips on duplicates; requestHash is the safety net.
access-token — signed proof-of-payment
After a successful settlement, the merchant issues a short-lived signed token from the receipt. The client presents the token on follow-up requests within a TTL to bypass payment for the same resource.
The token rides in a custom header (X-X402-Access-Token) so it is orthogonal to the core wire format. The token is HMAC-signed by the merchant: only the merchant can issue or verify; clients treat it as opaque.
Issuing
import { issueAccessToken } from "@nerochain/x402-extensions";
// Inside the merchant's settlement-success branch, after readSettlementReceipt(...):
const token = issueAccessToken({
receipt: {
payer: settlement.payer,
payTo: settlement.payTo,
transactionHash: settlement.transactionHash,
},
secret: process.env.MERCHANT_ACCESS_TOKEN_SECRET!, // ≥ 32 bytes of entropy
ttlSeconds: 3600, // 1 hour
scope: ["GET /api/article/123"], // optional
});
res.setHeader("X-X402-Access-Token", token);
res.json({ content: "..." });Verifying
import {
ACCESS_TOKEN_HEADER,
verifyAccessToken,
} from "@nerochain/x402-extensions";
const tokenHeader = req.headers[ACCESS_TOKEN_HEADER.toLowerCase()];
if (typeof tokenHeader === "string") {
const result = verifyAccessToken(
tokenHeader,
process.env.MERCHANT_ACCESS_TOKEN_SECRET!,
);
if (result.valid) {
// result.claims = { iss, sub, iat, exp, txh, scope? }
// Skip the payment gate; serve the resource directly.
return next();
}
// result.reason ∈ { "malformed", "signature_mismatch", "expired" }
}
// fall through to normal x402 gateSecurity model
- Secret stays merchant-side. Only the merchant issues and verifies. A leaked secret means an attacker can mint tokens for any payer; rotate the secret if compromised.
- Tokens are bound to the original settlement.
claims.txhrecords the on-chain hash that authorized the token; a compliant verifier can additionally check that the transaction is real and matchesclaims.sub(payer) andclaims.iss(merchant) on chain. - TTL bounds blast radius. Short TTLs (hours, not days) limit the impact of a leaked token.
- Scope is advisory. The token's
scopearray is application-level metadata; the merchant's verifier decides how to enforce it.
This is a deliberately minimal mechanism. CAIP-122 / SIWX with full session management is a richer extension worth doing later; for typical "pay once, read for an hour" patterns, signed access tokens are enough.
Combining the two
A common flow on a paid endpoint with caching:
- Client sends a request with
X-X402-Access-Tokenif it has one. - Server verifies the token. On success, serve the response — no payment.
- On failure (missing/expired/invalid), gate the request as usual: emit 402, accept payment, settle.
- After settlement, issue a fresh access token. Optionally include a
paymentIdentifierin the payload to deduplicate retries of the same logical paid request before reaching the bundler. - Return the response with both
PAYMENT-RESPONSE(settlement receipt) andX-X402-Access-Token(cached-access grant) headers.
The client's next call lands in step 2 and skips the payment gate until the token expires.