Paying a Challenge
Sign and submit an x402 payment challenge with pay() using a viem LocalAccount, then read the settlement receipt returned by the platform.
As the payer, you sign an x402 payment challenge with your own wallet key and submit it for settlement with a single pay() call. The signing is local and non-custodial: the key never leaves your process.
For payment challenges delivered over an email thread instead of a synthetic id, use email-native x402 payments instead, pay() here is for the out-of-band challenge flow from charging and registering payout addresses.
Prefer pay() over hand-rolling the signature. Drop to the low-level signing primitives only when pay() doesn't fit your signing flow (hardware wallet, custom submission path).
Prerequisites#
You need the SDK, a challenge object from the payee, a payer private key in the environment, and viem for the signer.
@primitivedotdev/sdkinstalled, per the Node.js SDK quickstart.- A payment challenge object, obtained from the payee's
charge()call (see charging and registering payout addresses). - A payer wallet private key, set as
PAYER_KEYin your environment. vieminstalled (npm install viem) forprivateKeyToAccount.
Sign and submit the payment#
- 1
Construct the x402 client#
Import
createX402Clientfrom thex402subpath (or callprimitive.x402(...)from the root import):// pay.ts import { createX402Client } from "@primitivedotdev/sdk/x402"; const x402 = createX402Client({ apiKey: process.env.PRIMITIVE_API_KEY! });Each request has a 30000 ms timeout by default; pass
timeoutMstocreateX402Clientto change it. - 2
Build a signer from your private key#
Use viem's
privateKeyToAccountto turnPAYER_KEYinto aLocalAccount.pay()calls itssignTypedDatamethod; the key stays in your process the whole time.// pay.ts (continued) import { privateKeyToAccount } from "viem/accounts"; const payer = privateKeyToAccount(process.env.PAYER_KEY as `0x${string}`); - 3
Call pay() with the challenge and signer#
challengeis the object the payee handed you (over email, an API call, or any out-of-band channel). Pass it straight topay():// pay.ts (continued) const receipt = await x402.pay(challenge, { signer: payer }); console.log(receipt.status, receipt.settle_tx); // "settled", "0x..." on-chain tx hashpay()derives the interaction-bound EIP-3009 nonce, assembles theTransferWithAuthorizationpayload, signs it withpayer.signTypedData, and submits it to the platform. The platform verifies every signed field against its own records, checks the org's spend policy if applicable, and settles on chain.
Verification signal: receipt.status reads "settled" and receipt.settle_tx carries a non-null on-chain transaction hash.
Re-hydrating a challenge to retry#
Call getChallenge(id) to re-fetch a challenge by id, for example after a process restart, instead of re-requesting it from the payee.
// retry.ts (x402 and payer constructed as above)
const challenge = await x402.getChallenge(challengeId);
const receipt = await x402.pay(challenge, { signer: payer });
Handling errors#
pay() throws X402Error on any client-side, transport, or non-2xx server error, carrying the HTTP status, the parsed error body, and any Retry-After value.
| Field | Meaning |
|---|---|
status | HTTP status number, or 0 for a client-side, transport, or timeout error that never reached the server |
body | The parsed error envelope, when present |
retryAfter | The Retry-After response header as a string, or null when the server sent none |
// pay.ts (continued)
import { X402Error } from "@primitivedotdev/sdk/x402";
try {
const receipt = await x402.pay(challenge, { signer: payer });
} catch (err) {
if (err instanceof X402Error) {
console.error(err.status, err.message, err.body);
}
}
A status === 0 error on pay() means the request may not have been sent at all, the payment outcome is indeterminate. Don't assume the payment failed; check getChallenge(id) or the org's payment history before retrying, since retrying a payment that actually went through is not idempotent in the way charge() is.
For the full error catalog, see Node.js SDK Errors.
Next steps#
Create the challenge this page pays, as the payee.
Email-Native x402 PaymentsPay a challenge that rides a real email thread instead of a synthetic id.
Low-Level x402 Signing PrimitivesDrive nonce derivation and signing yourself when pay() doesn't fit your flow.
Node.js SDK ErrorsLook up X402Error and every other error type the SDK raises.
Was this page helpful?