---
title: "Paying a Challenge"
canonical: "https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-paying"
markdown_url: "https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-paying.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Node.js SDK"
parent: "x402-payments-overview-04a296ff"
description: "Call x402.pay(challenge, { signer }) with a viem LocalAccount to sign and settle an x402 payment challenge and read the settlement receipt."
keywords: ["x402.pay()", "createX402Client", "viem LocalAccount", "X402Error", "settle_tx", "getChallenge"]
last_modified: "2026-08-11T18:54:56.123229+00:00"
published_at: "2026-08-11T18:54:55.97204+00:00"
source_files:
  - "sdk-node/README.md"
  - "sdk-node/src/x402/client.ts"
  - "sdk-go/x402.go"
  - "sdk-go/x402_test.go"
  - "sdk-go/README.md"
sections:
  - {anchor: "prerequisites", title: "Prerequisites"}
  - {anchor: "sign-and-submit-the-payment", title: "Sign and submit the payment"}
  - {anchor: "step-construct-the-x402-client", title: "Construct the x402 client"}
  - {anchor: "step-build-a-signer-from-your-private-key", title: "Build a signer from your private key"}
  - {anchor: "step-call-pay-with-the-challenge-and-signer", title: "Call pay() with the challenge and signer"}
  - {anchor: "re-hydrating-a-challenge-to-retry", title: "Re-hydrating a challenge to retry"}
  - {anchor: "handling-errors", title: "Handling errors"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# 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](https://test.abhinandan.one/x402-payments-overview.md) 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](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-email.md) instead, `pay()` here is for the out-of-band challenge flow from [charging and registering payout addresses](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-charging.md).

> **Tip:** Prefer `pay()` over hand-rolling the signature. Drop to the [low-level signing primitives](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-signing-primitives.md) 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/sdk` installed, per the [Node.js SDK quickstart](https://test.abhinandan.one/node-sdk-quickstart.md).
- A payment challenge object, obtained from the payee's `charge()` call (see [charging and registering payout addresses](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-charging.md)).
- A payer wallet private key, set as `PAYER_KEY` in your environment.
- `viem` installed (`npm install viem`) for `privateKeyToAccount`.

## Sign and submit the payment

### 1. Construct the x402 client

Import `createX402Client` from the `x402` subpath (or call `primitive.x402(...)` from the root import):

```typescript
// 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. 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.

```typescript
// 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

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

```typescript
// 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](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-spend-policy.md) 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.

```typescript
// 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 |

```typescript
// 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](https://test.abhinandan.one/node-sdk-errors.md).
