NERONERO 402
SDK

@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-extensions

payment-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 gate

Security 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.txh records the on-chain hash that authorized the token; a compliant verifier can additionally check that the transaction is real and matches claims.sub (payer) and claims.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 scope array 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:

  1. Client sends a request with X-X402-Access-Token if it has one.
  2. Server verifies the token. On success, serve the response — no payment.
  3. On failure (missing/expired/invalid), gate the request as usual: emit 402, accept payment, settle.
  4. After settlement, issue a fresh access token. Optionally include a paymentIdentifier in the payload to deduplicate retries of the same logical paid request before reaching the bundler.
  5. Return the response with both PAYMENT-RESPONSE (settlement receipt) and X-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.

On this page