{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"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","article":{"id":"05646c46-cbc5-4f54-a565-139f2b17e568","article_slug":"node-sdk-x402-charging","parent_article_slug":"x402-payments-overview-04a296ff","parent_article_title":"x402 Payments Overview","kind":"guide","published_at":"2026-08-11T18:54:55.302132+00:00","keywords":["registerPayoutAddress","charge()","createX402Client","x402 payment challenge","amountUsdc","payout address registration"],"meta_description":"Call registerPayoutAddress once to prove wallet ownership, then charge() to issue an x402 payment challenge as the payee using @primitivedotdev/sdk.","og_image_url":null,"source_file_paths":["sdk-node/README.md","sdk-node/src/x402/client.ts","sdk-node/src/x402/sign.ts"],"recording_id":null,"replayable":false,"task_name":"Charging and Registering Payout Addresses","category":"Node.js SDK","summary":null,"description":"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.","content_kind":"repo_page","content_markdown":"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`.\n\nFor the full non-custodial payment model (how signing, settlement, and spend policy fit together across every SDK), see [x402 Payments Overview](x402-payments-overview). This page covers only the payee-side calls: `registerPayoutAddress` and `charge()`.\n\n<Note>\n\nIf the payment needs to ride a real email thread instead of an out-of-band challenge id, use [Email-Native x402 Payments](node-sdk-x402-email) instead. This page covers the synthetic-challenge flow.\n\n</Note>\n\n## Construct the x402 client\n\nBuild the client from the `x402` subpath export. It defaults to reading `PRIMITIVE_API_KEY` from the environment.\n\n```ts\nimport { createX402Client } from \"@primitivedotdev/sdk/x402\";\n\nconst x402 = createX402Client({ apiKey: process.env.PRIMITIVE_API_KEY! });\n```\n\n## Register a payout address (one time)\n\n`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.\n\n<Steps>\n\n<Step title=\"Hold the payee's private key in a viem LocalAccount\">\n\nThe signing key never leaves your process. A viem `LocalAccount` built with `privateKeyToAccount` satisfies the signer interface `registerPayoutAddress` expects (it uses the account's `signMessage`).\n\n```ts\nimport { privateKeyToAccount } from \"viem/accounts\";\n\nconst payee = privateKeyToAccount(process.env.PAYEE_KEY as `0x${string}`);\n```\n\n</Step>\n\n<Step title=\"Call registerPayoutAddress\">\n\nPass 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).\n\n```ts\nimport { createX402Client } from \"@primitivedotdev/sdk/x402\";\nimport { privateKeyToAccount } from \"viem/accounts\";\n\nconst x402 = createX402Client({ apiKey: process.env.PRIMITIVE_API_KEY! });\nconst payee = privateKeyToAccount(process.env.PAYEE_KEY as `0x${string}`);\n\nawait x402.registerPayoutAddress(\n  { network: \"base-sepolia\", label: \"treasury\" },\n  { signer: payee },\n);\n```\n\n</Step>\n\n<Step title=\"Verify the registration\">\n\nList your registered payout addresses to confirm the new default landed:\n\n```ts\nconst addresses = await x402.listPayoutAddresses();\nconsole.log(addresses);\n```\n\nEach 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.\n\n</Step>\n\n</Steps>\n\n<Tip>\n\nRegistration is per network. If you also collect on mainnet, run the same `registerPayoutAddress` call again with `network: \"base\"` and a signer for that wallet.\n\n</Tip>\n\n## Create a payment challenge with charge()\n\n`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.\n\n```ts\nconst challenge = await x402.charge({\n  amountUsdc: \"0.01\", // human USDC amount\n  network: \"base-sepolia\",\n  payerOrg: process.env.PAYER_ORG_ID, // org allowed to pay this challenge\n  description: \"API call\",\n});\n```\n\n**Expected result**: `challenge` is an `X402Challenge` object with fields including `id`, `network`, `amount` (base units), `pay_to`, `nonce_binding`, `payment_requirements`, and `expires_at`.\n\n### Amount: human USDC vs. base units\n\nPass exactly one of:\n\n| Field | Format | Example | Notes |\n|---|---|---|---|\n| `amountUsdc` | human USDC decimal string | `\"0.01\"` | Recommended default. Converted to base units for you. |\n| `amount` | token base units | `\"10000\"` | USDC has 6 decimals, so `\"10000\"` = 0.01 USDC. Use only if you already have a base-unit value. |\n\nPassing both, or neither, throws an `X402Error` before any network call.\n\n### charge() options\n\n| Option | Required | Description |\n|---|---|---|\n| `amountUsdc` / `amount` | one of the two | See table above. |\n| `network` | no | `\"base-sepolia\"` (testnet, used in examples) or `\"base\"` (mainnet). Defaults to `\"base-sepolia\"`. |\n| `payerOrg` | no | The org id allowed to pay this challenge (on-net binding). |\n| `description` | no | Human-readable description shown to the payer. |\n| `resource` | no | A URL identifying the thing being paid for. |\n| `expiresIn` | no | Seconds until the challenge expires. Defaults to 1 hour. |\n| `idempotencyKey` | no | Retrying `charge()` with the same key returns the original challenge instead of creating a duplicate. |\n\n<Warning>\n\n`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 \"...\"`.\n\n</Warning>\n\n## Hand the challenge to the payer\n\nDeliver 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](node-sdk-x402-paying).\n\n```ts\n// Example: return the challenge from your own API route\nreturn Response.json(challenge);\n```\n\nIf the payer's process restarts before paying, re-hydrate the challenge by id instead of reissuing it:\n\n```ts\nconst challenge = await x402.getChallenge(challengeId);\n```\n\n## Errors\n\nEvery method on `X402Client` throws `X402Error` on a client-side, transport, or non-2xx server error. It carries:\n\n- `status`: the HTTP status, or `0` for a request that never reached the server (validation failures, malformed input, network errors).\n- `body`: the parsed error envelope when present.\n- `retryAfter`: the `Retry-After` header value, when the server sent one.\n\n```ts\nimport { X402Error } from \"@primitivedotdev/sdk/x402\";\n\ntry {\n  await x402.charge({ amountUsdc: \"0.01\" });\n} catch (err) {\n  if (err instanceof X402Error) {\n    console.error(err.status, err.message, err.retryAfter);\n  }\n}\n```\n\nFor the full error catalog across payments, webhooks, and email, see [Node.js SDK Errors](node-sdk-errors).\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Paying a Challenge\" href=\"node-sdk-x402-paying\">\n\nSign and submit payment for the challenge you just created, as the payer.\n\n</Card>\n\n<Card title=\"Email-Native x402 Payments\" href=\"node-sdk-x402-email\">\n\nIssue the same kind of challenge over a real email thread instead of an out-of-band id.\n\n</Card>\n\n<Card title=\"Spend Policy and Payout Address Management\" href=\"node-sdk-x402-spend-policy\">\n\nGuard outbound payments with caps, an allowlist, and a kill-switch, and list registered payout addresses.\n\n</Card>\n\n<Card title=\"Low-Level x402 Signing Primitives\" href=\"node-sdk-x402-signing-primitives\">\n\nDrive nonce derivation and payload assembly yourself when the high-level flow doesn't fit.\n\n</Card>\n\n</CardGroup>","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":"https://github.com/abhi-browzer/primitive-sdks/edit/main/sdk-node/README.md","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Charging+and+Registering+Payout+Addresses&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fnode-sdk-x402-charging","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}