Skip to main content

Buyer guide: pay for an API

:::tip Any wallet with USDC on XDC can pay You are the buyer: you hold USDC and sign a payment. There is no account to create, no API key, and no XDC AI wallet required. Bring MetaMask, Rabby, a Ledger, or a bare private key. Seller API keys and credits belong to the seller side. :::

Gas is sponsored, so you never need XDC to pay for a call. You sign a message, and the facilitator submits the transaction and pays the gas.

Which wallets can pay​

Anything that can sign EIP-712 typed data works, and nothing has to be registered anywhere first:

WalletPays?
MetaMask, Rabby, Trust, any browser or mobile walletYes
A hardware wallet (Ledger, Trezor)Yes
A raw private key in a script or a backendYes
An XDC AI hosted wallet from xdcai loginYes
An MPC wallet that returns a standard ECDSA signatureYes
A deployed smart contract wallet (ERC-1271)Yes, see the adapter
A contract wallet that is not deployed yet (ERC-6492)No

USDC on XDC checks signatures with OpenZeppelin's SignatureChecker, so it accepts an ordinary ECDSA signature and a contract account's ERC-1271 signature through the same call. A deployed contract wallet signs its own EIP-3009 authorization and settles through the public facilitator; it does not have to move funds to an ordinary address first. The wallet contract is the payer, and its signature must be passed through unchanged. See Smart contract wallets.

Pick your path​

Both end in the same place: a signed authorization on the seller's second request. They differ only in how much you write. If you would rather not write code at all, the CLI and the MCP connector make paid calls for you.

@xdcai/x402-buyer is a payment-aware fetch for your own Node agent, with a spending policy you set and an idempotent payment-intent store. It is a thin, XDC-pinned wrapper over the official @x402/core, @x402/evm and @x402/fetch packages, published by the facilitator team (Apache-2.0, Node 20+). You pass a signer, never a raw key.

npm install @xdcai/x402-buyer@1.0.0-rc.2

:::warning Pin the exact version This is a release candidate, and npm latest still points at 1.0.0-rc.1 while next points at 1.0.0-rc.2. Pin the exact version rather than a floating range, or you get whichever one a tag happens to mean that week. rc.2 changed documentation only: its compiled output is identical to rc.1, so upgrading fixes nothing and breaks nothing. :::

import { createXdcPaymentFetch, XDC_USDC } from "@xdcai/x402-buyer";

// signer is a ClientEvmSigner: an object with an address and signTypedData,
// for example a viem LocalAccount. Do not paste a private key into source or chat.
const paidFetch = createXdcPaymentFetch({
signer,
maxAmount: 10_000n, // atomic USDC units (6 decimals), so this is 0.01 USDC
allowedAssets: [XDC_USDC],
allowedPayTo: ["0xYourTrustedSeller"],
});

const response = await paidFetch("https://seller.example/api/data", {
paymentIntentId: "order-123", // required, one logical purchase
});

If the response is a 402 within policy, the SDK selects the requirement, asks your signer to sign, retries with the proof, and validates the receipt before returning. If anything is out of policy it throws before the signer is ever called.

The policy is the point. Every guard is on by default or required, and each refuses before signing rather than after paying:

ControlBehavior
maxAmountRequired. A bigint in atomic units, not fiat. There is no price oracle.
NetworkDefaults to and requires eip155:50. allowedNetworks to change.
AssetDefaults to XDC USDC. allowedAssets to change.
allowedPayToOptional payee allowlist. Set it for production spend.
Caller-supplied proofRejected. The SDK attaches the payment header, you do not.
RedirectsNever followed, so a proof cannot leak to another origin.

Idempotency. Every call needs a paymentIntentId, your key for one logical purchase, and it is what stops a retry from paying twice. The SDK tracks each intent as in_flight, then ambiguous or completed, in a PaymentIntentStore.

:::danger Inject a durable store in production The default createMemoryPaymentIntentStore() is single-process and forgets on restart, so two workers could both authorize the same intent. For any real deployment pass an intentStore backed by a database with an insert-if-absent or CAS reserve. The store interface is exported for you to implement. :::

When settlement is uncertain. If a request is interrupted after the authorization went out but before a confirmed receipt, the intent lands in ambiguous. The SDK will not guess, so you inspect and reconcile:

await paidFetch.listAmbiguousAttempts();
await paidFetch.reconcileAmbiguousAttempt(attemptId, { outcome: "not_settled" });
// or, if it did settle:
await paidFetch.reconcileAmbiguousAttempt(attemptId, { outcome: "settled", transaction: "0x..." });

A stranded in_flight reservation is fail-closed: the SDK will not release it (that would reopen a double-payment race). You can mark it settled if you confirm it settled; recovering one that did not settle is an operator action on the durable store, after confirming the owning process is dead.

Common BuyerSdkError codes: amount_exceeds_limit, unsupported_network / unsupported_asset, payee_not_allowed, signer_refused, caller_supplied_signature, ambiguous_payment_state.

Real-money calls should require an explicit human approval before the first live payment, and should show the network, asset, payee and maximum amount first.

Getting your receipt​

A successful paid call returns a receipt header, PAYMENT-RESPONSE on v2 or X-PAYMENT-RESPONSE on v1, holding the settlement result including the transaction hash. The Buyer SDK reads and validates it for you before returning. If the seller omits it, the payment can still be found on-chain by the nonce you signed: the USDC contract emits AuthorizationUsed(authorizer, nonce).