Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /x402-payments-overview-04a296ff/node-sdk-x402-paying.md.
Verified · 8/11/2026

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.

Tip

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.

Sign and submit the payment#

  1. 1

    Construct the x402 client#

    Import createX402Client from the x402 subpath (or call primitive.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 timeoutMs to createX402Client to change it.

  2. 2

    Build a signer from your private key#

    Use viem's privateKeyToAccount to turn PAYER_KEY into a LocalAccount. pay() calls its signTypedData method; 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. 3

    Call pay() with the challenge and signer#

    challenge is the object the payee handed you (over email, an API call, or any out-of-band channel). Pass it straight to pay():

    // pay.ts (continued)
    const receipt = await x402.pay(challenge, { signer: payer });
    
    console.log(receipt.status, receipt.settle_tx); // "settled", "0x..." on-chain tx hash
    

    pay() derives the interaction-bound EIP-3009 nonce, assembles the TransferWithAuthorization payload, signs it with payer.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.

FieldMeaning
statusHTTP status number, or 0 for a client-side, transport, or timeout error that never reached the server
bodyThe parsed error envelope, when present
retryAfterThe 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);
  }
}
Warning

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#

Was this page helpful?

© Primitive SDKs

Powered by Browzer