/** x402 V2 — the paying half of the agent rail, in the SDK. * * This module is a function-for-function port of the server's * `api/src/services/economy/x402-client.ts` (parse → refuse → sign) plus the * wire helpers it borrows from `api/src/middleware/x402.ts` (canonical * base64, strict `PaymentRequirements` parsing, USDC pins) and a local * EIP-712 / secp256k1 signer built on `@noble/curves` + `@noble/hashes`, * which the SDK already ships. No new dependency. The server is normative: * `tests/fixtures/x402-eip3009-vector.json` was produced by the server and * viem, and the signer here must reproduce it byte for byte. * * ## Doctrine (changed deliberately, Wave 2 Phase C) * * The SDK CAN sign and pay on 402 — but only when the caller opts in with an * explicit signer AND a spend policy. Never by default. A policy without a * hard cap (`maxAmountAtomic`) and a recipient allow-list (`allowedPayTo`) * is not a policy and is rejected before anything is signed. Allow-lists, * never deny-lists: a 402 body is untrusted input from whoever we are * talking to; it must never be able to introduce a new asset contract, a * new network, or a new recipient. * * ## The walls (inherited from the server client) * * 1. **A cap that is not advisory.** An over-cap requirement is refused * (`amount_over_cap`), never clamped. Paying less than asked produces an * authorization the counterparty rejects, which reads as our bug rather * than their price. * 2. **Allow-lists, not deny-lists.** Network, asset and payTo must be named * in advance. * 3. **The narrowest validity window that satisfies the requirement.** A * signed EIP-3009 authorization is bearer-spendable until `validBefore`; * a long window is a long liability. * 4. **No re-signing. Ever.** `signExactEvmAuthorization` mints a fresh * random nonce every call, so it cannot be used as a retry mechanism. A * caller that must retry replays the identical bytes it already holds * (`paymentIsStillReplayable`); a caller that calls again is authorizing * a second, independent payment. * * ## What this module does NOT do * * The protocol functions do no network. They parse, refuse, and sign; the * paying transport lives in `_x402-transport.ts` (Phase C, W2-7) and is * installed only by the `x402` client option. `X402Client` (`at.x402`, at * the bottom of this file) is the one namespace that talks to the API — the * purchase door and the payment-status door — and it pays only through that * same opt-in transport. No key custody beyond what the caller hands * `localEvmSigner` — the private key is closed over and never leaves the * returned signer. * * Server: api/src/services/economy/x402-client.ts · api/src/middleware/x402.ts * Verifier: api/src/services/economy/x402-payments.ts (classifyExactEvmSignature) * Plan: docs/superpowers/plans/2026-08-29-wave-2-agent-rail.md (W2-6) */ import { type HttpConfig } from "./_http.js"; import { type X402PaymentRequirement, type X402ResourceInfo } from "./errors.js"; export declare const X402_VERSION: 2; /** Production supports EIP-3009 only. Network identifiers are CAIP-2. */ export type X402Network = "eip155:8453" | "eip155:84532" | "eip155:137" | "eip155:42161"; export interface X402AssetDefinition { asset: string; name: string; version: string; } /** USDC per network — same values the server pins in `middleware/x402.ts` * (x402-foundation/x402 commit 0a604079aca7b5a45a2e1620ba444e13982646c8). */ export declare const X402_USDC_ASSETS: Readonly>; /** CAIP-2 for Base mainnet — the network the kingdom settles on. */ export declare const X402_BASE_NETWORK: X402Network; /** Circle's native USDC on Base. */ export declare const X402_BASE_USDC: string; /** The KINGDOM Sovereign Reserve treasury on Base. AgentTool's own 402s * direct payment here; name it in `allowedPayTo` to pay AgentTool and * nothing else. */ export declare const AGENTTOOL_TREASURY = "0xA9eeA60CAaF239AbAfAA05FcB152128dB16dD3d8"; /** 1 project credit = 1,000 USDC atomic units = USD 0.001. */ export declare const X402_ATOMIC_PER_CREDIT = 1000n; /** Encoded-size ceiling keeps parser calls bounded. */ export declare const MAX_X402_HEADER_B64_LENGTH: number; /** EIP-712 types for USDC-style `transferWithAuthorization`. Identical to the * server's verifier — the two sides must agree byte for byte, so the shape * is duplicated deliberately rather than shared. */ export declare const TRANSFER_WITH_AUTHORIZATION_TYPES: { readonly TransferWithAuthorization: readonly [{ readonly name: "from"; readonly type: "address"; }, { readonly name: "to"; readonly type: "address"; }, { readonly name: "value"; readonly type: "uint256"; }, { readonly name: "validAfter"; readonly type: "uint256"; }, { readonly name: "validBefore"; readonly type: "uint256"; }, { readonly name: "nonce"; readonly type: "bytes32"; }]; }; /** x402 V2 PaymentRequired — the 402 body and the PAYMENT-REQUIRED header. */ export interface X402PaymentRequired { x402Version: typeof X402_VERSION; error?: string; resource: X402ResourceInfo; accepts: X402PaymentRequirement[]; extensions?: Record; } /** x402 V2 PaymentPayload, carried by PAYMENT-SIGNATURE. */ export interface X402PaymentPayload { x402Version: typeof X402_VERSION; resource?: X402ResourceInfo; accepted: X402PaymentRequirement; payload: Record; extensions?: Record; } /** x402 V2 SettleResponse, carried by PAYMENT-RESPONSE. */ export interface X402SettleResponse { success: boolean; errorReason?: string; errorMessage?: string; payer?: string; transaction: string; network: string; amount?: string; extensions?: Record; extra?: Record; } /** The six EIP-3009 fields — what decides where the money goes. */ export interface X402Authorization { from: string; to: string; value: string; validAfter: string; validBefore: string; nonce: string; } /** What this instance is willing to spend, before it sees any challenge. * Every field is a refusal condition, not a preference, and every field is * mandatory: there are no defaults, because a default is a decision the * caller did not make. */ export interface X402SpendPolicy { /** Hard per-payment ceiling in the asset's atomic units. Over-cap is * refused, never clamped. */ maxAmountAtomic: bigint; /** Recipients we will sign for. Compared case-insensitively. A 402 cannot * redirect payment anywhere not named here. */ allowedPayTo: readonly string[]; /** CAIP-2 networks we will sign for. */ allowedNetworks: readonly X402Network[]; /** Asset contract addresses we will sign for. Compared case-insensitively. */ allowedAssets: readonly string[]; /** Longest authorization validity we will mint, in seconds. The signed * window is `min(requirement.maxTimeoutSeconds, this)`. */ maxValiditySeconds: number; } export type X402ClientRefusalReason = "not_a_payment_required_body" | "no_acceptable_requirement" | "network_not_allowed" | "asset_not_allowed" | "pay_to_not_allowed" | "amount_over_cap" | "unsupported_transfer_method" | "validity_window_unusable"; export interface X402ClientRefusal { ok: false; reason: X402ClientRefusalReason; /** One sentence an operator can act on. */ detail: string; } export interface X402SelectedRequirement { ok: true; requirement: X402PaymentRequirement; amountAtomic: bigint; } /** EIP-712 payload handed to the signer. Structurally plain so any signer — * `localEvmSigner`, viem, a hardware wallet — can consume it. */ export interface TransferWithAuthorizationTypedData { domain: { name: string; version: string; chainId: number; verifyingContract: string; }; types: typeof TRANSFER_WITH_AUTHORIZATION_TYPES; primaryType: "TransferWithAuthorization"; message: { from: string; to: string; value: bigint; validAfter: bigint; validBefore: bigint; nonce: string; }; } /** Whoever holds the key. `address` is the payer (`from`); `signTypedData` * returns a 65-byte `0x r‖s‖v` hex signature. */ export interface X402Signer { readonly address: string; signTypedData(typedData: TransferWithAuthorizationTypedData): Promise; } export interface SignedX402Payment { /** Ready for the PAYMENT-SIGNATURE header. */ header: string; payload: X402PaymentPayload; /** Stable identity of the authorization these bytes carry. * * Persist this BEFORE emitting the request. If the response is ambiguous, * recovery is a lookup on what was emitted — not a fresh signature. */ authorizationHash: string; /** Unix seconds after which these bytes are dead. */ validBefore: number; } /** Decode canonical standard base64 (padded, no whitespace, re-encodes to * itself). Returns null for anything else — the same strictness the server * applies to inbound headers. */ export declare function decodeCanonicalBase64(value: string, maxEncodedLength?: number): Uint8Array | null; /** `base64(JSON.stringify(value))` — byte-identical to the server's * `Buffer.from(JSON.stringify(value), "utf-8").toString("base64")`. */ export declare function encodeCanonicalBase64Json(value: unknown): string; /** keccak-256 (the Ethereum one — NOT SHA3-256; the padding differs). */ export declare function keccak256(data: Uint8Array): Uint8Array; /** EIP-55 mixed-case checksum of a 20-byte hex address. */ export declare function checksumEvmAddress(address: string): string; /** Same acceptance as viem's `isAddress` (strict): 20-byte hex, and if it * carries mixed case the EIP-55 checksum must be right. The server's * parser uses exactly this, so a payTo or asset the server would reject is * rejected here too. */ export declare function isEvmAddress(value: unknown): value is string; /** Strict `ResourceInfo`: only the spec's keys, url required and bounded. */ export declare function parseResourceInfo(value: unknown): X402ResourceInfo | null; /** Strict `PaymentRequirements`: the same parser the server runs on inbound * headers, so a counterparty cannot hand us a shape our own verifier would * reject. */ export declare function parsePaymentRequirements(value: unknown): X402PaymentRequirement | null; /** Parse an untrusted 402 body (or a decoded PAYMENT-REQUIRED header) into a * `PaymentRequired`. Guidance keys the API adds to its 402 body (message, * hint, next_actions, …) are ignored; every entry in `accepts` must pass * `parsePaymentRequirements`, and `resource` must pass `parseResourceInfo` * because it is echoed back in PAYMENT-SIGNATURE and the verifier applies * that exact check. */ export declare function parsePaymentRequiredBody(value: unknown): X402PaymentRequired | null; /** Decode a PAYMENT-REQUIRED header into a strict `PaymentRequired`, or * null for anything that is not canonical base64 of a valid envelope. */ export declare function decodePaymentRequiredHeader(headerValue: string): X402PaymentRequired | null; /** Decode a PAYMENT-RESPONSE header into a `SettleResponse`, or null. The * receipt is the facilitator's word, not the ledger's: `success: true` * here proves settlement was reported, not that credits were applied. */ export declare function decodePaymentResponseHeader(headerValue: string): X402SettleResponse | null; /** Pick the first requirement this policy permits, or say precisely why none * qualified. * * "First permitted", not "cheapest": the counterparty orders `accepts` by * its own preference, and reordering by price would quietly opt us into * whichever rail they listed last. Cost is bounded by the cap instead. */ export declare function selectPayableRequirement(required: X402PaymentRequired, policy: X402SpendPolicy): X402SelectedRequirement | X402ClientRefusal; /** Canonical identity of an authorization: the fields that decide where the * money goes. Two byte-identical emissions hash the same; any change to * recipient, amount, window, or nonce does not. Same bytes as the server's * `x402-client.ts` authorizationHash. */ export declare function authorizationHash(auth: X402Authorization): string; /** EIP-712 digest of a `TransferWithAuthorization` typed-data payload: * `keccak256(0x1901 ‖ domainSeparator ‖ hashStruct(message))`. What the * signer signs, and what viem's `hashTypedData` produces for the same input * (pinned by the fixture's `eip712_digest`). */ export declare function hashTransferWithAuthorization(typedData: TransferWithAuthorizationTypedData): Uint8Array; /** Checksummed EVM address for a secp256k1 private key: * `keccak256(uncompressedPubKey[1:])[12:]`. */ export declare function evmAddressFromPrivateKey(privateKeyHex: string): string; /** Recover the address that signed a `TransferWithAuthorization` payload. * Offline; the same check the server's verifier makes before it trusts a * signature (`classifyExactEvmSignature` → `eoa_verified`). Throws on a * malformed signature. */ export declare function recoverTypedDataAddress(typedData: TransferWithAuthorizationTypedData, signature: string): string; /** A signer that holds one secp256k1 private key in a closure. * * Signs EIP-712 `TransferWithAuthorization` payloads with deterministic * (RFC 6979) low-s ECDSA and returns Ethereum's `0x r‖s‖v` (v = 27 + recid) * — byte-identical to viem's `signTypedData` for the same input. It refuses * to sign a message whose `from` is not its own address: an authorization * from someone else is never right, and the verifier would only bounce it. * * The key never leaves the closure; the returned object exposes only the * address and the signing function. Use only with a payer wallet you intend * to spend from, under a policy that caps it. */ export declare function localEvmSigner(privateKeyHex: string): X402Signer; /** Sign one EIP-3009 authorization against a selected requirement. * * Every call mints a fresh random nonce. That is the wall, not an * implementation detail: because a second call can never reproduce the first * authorization, this function cannot be used as a retry mechanism. A caller * that needs to retry must replay the bytes it already holds, which a * conformant facilitator dedupes by nonce. A caller that calls again is * authorizing a second, independent payment, and the fresh nonce makes that * visible in the ledger instead of silent. * * The requirement is re-checked against the policy here as well, so calling * this directly cannot bypass the cap or the allow-lists: a refusal throws * an `AgentToolError` whose `code` is the refusal reason. * * `nowSeconds` is injected rather than read from the clock so the window is * testable and so a caller with a trusted time source can supply it. */ export declare function signExactEvmAuthorization(options: { requirement: X402PaymentRequirement; policy: X402SpendPolicy; signer: X402Signer; nowSeconds: number; resource?: X402ResourceInfo; }): Promise; /** True when these bytes can still be replayed. * * The safe response to an ambiguous failure is to re-send the identical * authorization until it expires — never to sign a new one. */ export declare function paymentIsStillReplayable(signed: SignedX402Payment, nowSeconds: number): boolean; /** Result of `at.x402.topUp(credits)` — the server's receipt, camel-cased, * plus the raw settlement headers the response carried. */ export interface X402TopUpResult { /** Credits the server applied in the same transaction that recorded settlement. */ creditsAdded: number; /** Project balance after the top-up, when the server reported it. */ creditsTotal: number | null; /** The server's LEDGER identity for this payment (folds network + asset); * what `at.x402.payment(id)` resolves. Not the client-side * `authorizationHash` of the six EIP-3009 fields. */ authorizationHash: string | null; /** Atomic USDC moved for this top-up (credits × 1,000). */ amountAtomic: string; /** The server's unit and finality sentences, verbatim. */ unit: string | null; finality: string | null; /** Project-scoped status path, when the server named one. */ paymentStatus: string | null; /** Raw `PAYMENT-RESPONSE` settlement receipt header, when present. */ paymentResponse?: string; /** Raw `Link` header (rel="payment-status"), when present. */ paymentStatusLink?: string; /** Raw `X-Credits-Balance` header, when present. */ creditsBalance?: string; } /** One row of `GET /v1/x402/payments/:id` — the project-scoped * reconciliation of the payment/credit lifecycle only. It never replays or * promises an exactly-once result for the paid request itself. Field names * are the server's. */ export interface X402PaymentStatus { payment_id: string; status: string; failure_reason: string | null; scheme: string; network: string; asset: string; amount: string; pay_to: string; max_timeout_seconds: number; requirement_extra: Record; resource: string; resource_info: Record | null; credits_purchased: number | null; authorization_evidence: Record; settlement_attempted_at: string | null; transaction: string | null; receipt: Record | null; credits_applied: number | null; reconciles: string; next_action: string; retry_after_seconds: number | null; environment_note: string | null; pending_note: string | null; updated_at: string | null; [key: string]: unknown; } export interface X402TopUpOptions { /** Opaque, already signed x402 V2 `PAYMENT-SIGNATURE` base64-JSON value, * for callers who sign outside the SDK. Sent as the header only; when * present the SDK's own payer (if configured) stays out of the way. */ paymentSignature?: string; /** `Idempotency-Key` for the top-up. Defaults to a fresh UUID per call. * The paying transport carries the same key on its signed retry, so an * ambiguous response retried with this key returns the stored 200, never * a second settlement. */ idempotency_key?: string; } /** `at.x402` — the agent rail's two doors. * * `topUp(credits)` knocks on `POST /v1/x402/top-up/:credits`. With no * `x402` option on the client the answer is the 402 challenge itself, as a * typed `AgentToolError` carrying `accepts` / `paymentRequired`, and nothing * is signed. With the option, the paying transport answers that 402 with * exactly one signed retry under the spend policy and this method returns * the receipt. `payment(id)` reads the ledger row behind a payment. * * Neither method signs anything itself. */ export declare class X402Client { private readonly http; constructor(http: HttpConfig); /** Buy `credits` project credits with USDC on Base (1 credit = 1,000 * atomic = USD 0.001). Final: no refunds; unspent credits stay. */ topUp(credits: number, options?: X402TopUpOptions): Promise; /** Read the project-scoped ledger row for a payment. `paymentId` is the * server's identity — `X402TopUpResult.authorizationHash`, the id inside * a rel="payment-status" Link, or `X402PaymentEvent.paymentId`. */ payment(paymentId: string): Promise; }