---
title: "Charging and Registering Payout Addresses"
canonical: "https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-charging"
markdown_url: "https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-charging.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Node.js SDK"
parent: "x402-payments-overview-04a296ff"
description: "Call registerPayoutAddress once to prove wallet ownership, then charge() to issue an x402 payment challenge as the payee using @primitivedotdev/sdk."
keywords: ["registerPayoutAddress", "charge()", "createX402Client", "x402 payment challenge", "amountUsdc", "payout address registration"]
last_modified: "2026-08-11T18:54:55.456272+00:00"
published_at: "2026-08-11T18:54:55.302132+00:00"
source_files:
  - "sdk-node/README.md"
  - "sdk-node/src/x402/client.ts"
  - "sdk-node/src/x402/sign.ts"
sections:
  - {anchor: "construct-the-x402-client", title: "Construct the x402 client"}
  - {anchor: "register-a-payout-address-one-time", title: "Register a payout address (one time)"}
  - {anchor: "step-hold-the-payees-private-key-in-a-viem-localaccount", title: "Hold the payee's private key in a viem LocalAccount"}
  - {anchor: "step-call-registerpayoutaddress", title: "Call registerPayoutAddress"}
  - {anchor: "step-verify-the-registration", title: "Verify the registration"}
  - {anchor: "create-a-payment-challenge-with-charge", title: "Create a payment challenge with charge()"}
  - {anchor: "amount-human-usdc-vs-base-units", title: "Amount: human USDC vs. base units"}
  - {anchor: "charge-options", title: "charge() options"}
  - {anchor: "hand-the-challenge-to-the-payer", title: "Hand the challenge to the payer"}
  - {anchor: "errors", title: "Errors"}
  - {anchor: "next-steps", title: "Next steps"}
---

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

# Charging and Registering Payout Addresses

Register your wallet as a payout destination and issue x402 payment challenges with charge(), the payee-side half of Primitive's non-custodial USDC payments.

Use this guide when you're the **payee**: you want to get paid in USDC and need to (1) tell Primitive where to send funds, and (2) issue a payment request another agent can sign and pay. Both steps use the `X402Client` from `@primitivedotdev/sdk/x402`.

For the full non-custodial payment model (how signing, settlement, and spend policy fit together across every SDK), see [x402 Payments Overview](https://test.abhinandan.one/x402-payments-overview.md). This page covers only the payee-side calls: `registerPayoutAddress` and `charge()`.

> **Note:** If the payment needs to ride a real email thread instead of an out-of-band challenge id, use [Email-Native x402 Payments](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-email.md) instead. This page covers the synthetic-challenge flow.

## Construct the x402 client

Build the client from the `x402` subpath export. It defaults to reading `PRIMITIVE_API_KEY` from the environment.

```ts
import { createX402Client } from "@primitivedotdev/sdk/x402";

const x402 = createX402Client({ apiKey: process.env.PRIMITIVE_API_KEY! });
```

## Register a payout address (one time)

`registerPayoutAddress` proves control of a wallet by signing an ownership message with the wallet's own key, then registers that address as your org's default payout destination on a given network. `charge()` resolves its `pay_to` field from this registered address, so register before your first `charge()` call.

### 1. Hold the payee's private key in a viem LocalAccount

The signing key never leaves your process. A viem `LocalAccount` built with `privateKeyToAccount` satisfies the signer interface `registerPayoutAddress` expects (it uses the account's `signMessage`).

```ts
import { privateKeyToAccount } from "viem/accounts";

const payee = privateKeyToAccount(process.env.PAYEE_KEY as `0x${string}`);
```

### 2. Call registerPayoutAddress

Pass the target `network` and an optional `label`. The ownership message binds your organization id, so a captured signature can never register the address under a different org. You don't pass `org` yourself; it's resolved automatically from your account (supply `org` only to override).

```ts
import { createX402Client } from "@primitivedotdev/sdk/x402";
import { privateKeyToAccount } from "viem/accounts";

const x402 = createX402Client({ apiKey: process.env.PRIMITIVE_API_KEY! });
const payee = privateKeyToAccount(process.env.PAYEE_KEY as `0x${string}`);

await x402.registerPayoutAddress(
  { network: "base-sepolia", label: "treasury" },
  { signer: payee },
);
```

### 3. Verify the registration

List your registered payout addresses to confirm the new default landed:

```ts
const addresses = await x402.listPayoutAddresses();
console.log(addresses);
```

Each entry carries `address`, `network`, `label`, `is_default`, and `verified_at`; look for the newly registered address on `base-sepolia` marked `is_default: true`. Once registered, every `charge()` on that network resolves `pay_to` to this address automatically, so you never pass a payout address to `charge()` directly.

> **Tip:** Registration is per network. If you also collect on mainnet, run the same `registerPayoutAddress` call again with `network: "base"` and a signer for that wallet.

## Create a payment challenge with charge()

`charge()` creates an x402 payment challenge: the object the payer signs and pays with `pay()`. It carries `payment_requirements` and `nonce_binding`, and Primitive fills in `pay_to` from your registered payout address.

```ts
const challenge = await x402.charge({
  amountUsdc: "0.01", // human USDC amount
  network: "base-sepolia",
  payerOrg: process.env.PAYER_ORG_ID, // org allowed to pay this challenge
  description: "API call",
});
```

**Expected result**: `challenge` is an `X402Challenge` object with fields including `id`, `network`, `amount` (base units), `pay_to`, `nonce_binding`, `payment_requirements`, and `expires_at`.

### Amount: human USDC vs. base units

Pass exactly one of:

| Field | Format | Example | Notes |
|---|---|---|---|
| `amountUsdc` | human USDC decimal string | `"0.01"` | Recommended default. Converted to base units for you. |
| `amount` | token base units | `"10000"` | USDC has 6 decimals, so `"10000"` = 0.01 USDC. Use only if you already have a base-unit value. |

Passing both, or neither, throws an `X402Error` before any network call.

### charge() options

| Option | Required | Description |
|---|---|---|
| `amountUsdc` / `amount` | one of the two | See table above. |
| `network` | no | `"base-sepolia"` (testnet, used in examples) or `"base"` (mainnet). Defaults to `"base-sepolia"`. |
| `payerOrg` | no | The org id allowed to pay this challenge (on-net binding). |
| `description` | no | Human-readable description shown to the payer. |
| `resource` | no | A URL identifying the thing being paid for. |
| `expiresIn` | no | Seconds until the challenge expires. Defaults to 1 hour. |
| `idempotencyKey` | no | Retrying `charge()` with the same key returns the original challenge instead of creating a duplicate. |

> **Warning:** `charge()` rejects unknown option keys (for example a typo like `payer_org` instead of `payerOrg`) with an `X402Error` at call time rather than silently dropping the field. Check spelling against the table above if you hit `unknown charge() option "..."`.

## Hand the challenge to the payer

Deliver the returned `challenge` object to the payer over any out-of-band channel: an API response, a dashboard, a queued message. The payer needs the whole object to sign and pay it; see [Paying a Challenge](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-paying.md).

```ts
// Example: return the challenge from your own API route
return Response.json(challenge);
```

If the payer's process restarts before paying, re-hydrate the challenge by id instead of reissuing it:

```ts
const challenge = await x402.getChallenge(challengeId);
```

## Errors

Every method on `X402Client` throws `X402Error` on a client-side, transport, or non-2xx server error. It carries:

- `status`: the HTTP status, or `0` for a request that never reached the server (validation failures, malformed input, network errors).
- `body`: the parsed error envelope when present.
- `retryAfter`: the `Retry-After` header value, when the server sent one.

```ts
import { X402Error } from "@primitivedotdev/sdk/x402";

try {
  await x402.charge({ amountUsdc: "0.01" });
} catch (err) {
  if (err instanceof X402Error) {
    console.error(err.status, err.message, err.retryAfter);
  }
}
```

For the full error catalog across payments, webhooks, and email, see [Node.js SDK Errors](https://test.abhinandan.one/node-sdk-errors.md).
