---
title: "x402 Payments Overview"
canonical: "https://test.abhinandan.one/x402-payments-overview-04a296ff"
markdown_url: "https://test.abhinandan.one/x402-payments-overview-04a296ff.md"
publisher: "Primitive SDKs"
kind: "concept"
content_type: "reference"
category: "Core Concepts"
description: "Registering a payout address, creating and paying an x402 payment challenge, and guarding sends with a spend policy work identically across every Primitive SDK."
keywords: ["x402 payment challenge", "charge()", "pay()", "registerPayoutAddress", "createEmailChallenge", "spend policy"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:54:53.187131+00:00"
source_files:
  - "README.md"
  - "sdk-node/README.md"
  - "sdk-python/README.md"
  - "sdk-go/README.md"
  - "cli-node/README.md"
  - "sdk-node/src/x402/client.ts"
  - "sdk-go/x402.go"
sections:
  - {anchor: "the-four-step-model", title: "The four-step model"}
  - {anchor: "networks-and-amounts", title: "Networks and amounts"}
  - {anchor: "registering-a-payout-address", title: "Registering a payout address"}
  - {anchor: "creating-a-challenge-payee", title: "Creating a challenge (payee)"}
  - {anchor: "paying-a-challenge-payer", title: "Paying a challenge (payer)"}
  - {anchor: "email-native-payments", title: "Email-native payments"}
  - {anchor: "spend-policy", title: "Spend policy"}
  - {anchor: "errors", title: "Errors"}
  - {anchor: "next-steps", title: "Next steps"}
---

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

# x402 Payments Overview

x402 is Primitive's non-custodial USDC payment model: a payee registers a payout address, requests a payment with charge(), and a payer signs and settles it locally with pay(), identically across the Node, Python, and Go SDKs and the CLI.

x402 is Primitive's non-custodial USDC payment protocol: one agent (the payee) requests a payment, and another agent (the payer) signs and settles it with their own wallet key. Primitive's servers never hold funds, the payer signs an EIP-3009 `transferWithAuthorization` locally, and the platform verifies every signed field against its own records before the transaction settles on chain.

The same model, the same four steps, and largely the same method names exist in the Node SDK (`@primitivedotdev/sdk/x402`), the Python SDK (`primitive.x402`), the Go SDK (`sdk-go`'s `X402Client`), and the CLI's `primitive payments` command group.

## The four-step model

1. **Register a payout address** (payee, one time). The payee signs an ownership message with their wallet key, proving control of the address. The recovered address becomes the org's default payout destination for that network.
2. **Create a payment challenge** (payee). `charge()` builds an **x402 payment challenge**: a request for payment that carries `payment_requirements` and a `nonce_binding`. The platform fills in `pay_to` from the address registered in step 1.
3. **Sign and pay the challenge** (payer). `pay()` signs the challenge's authorization locally with the payer's own key and submits it.
4. **The platform verifies and settles.** Every signed field is checked against the platform's own records, the org's spend policy is enforced, and the transaction settles on chain.

```mermaid
sequenceDiagram
    participant Payee
    participant Platform
    participant Payer

    Payee->>Platform: registerPayoutAddress (signed ownership proof)
    Payee->>Platform: charge() -> creates challenge
    Platform-->>Payee: challenge (payment_requirements, nonce_binding)
    Payee->>Payer: hand off challenge (API call, email, dashboard)
    Payer->>Payer: sign EIP-3009 authorization locally
    Payer->>Platform: pay(challenge, signer) -> submits signed payment
    Platform->>Platform: verify signed fields + spend policy
    Platform-->>Payer: receipt (status, settle_tx)
```

Keys never leave the caller in either direction: the payee's key signs only the payout-address ownership message, and the payer's key signs only the payment authorization. Neither key is ever sent to Primitive.

## Networks and amounts

x402 runs on two networks: `base-sepolia` (testnet, the default in every SDK's examples) and `base` (mainnet). USDC has 6 decimals, so amounts can be given either way:

- **Human USDC**: `amountUsdc: "0.01"` (Node), `amount_usdc="0.01"` (Python), `AmountUsdc: "0.01"` (Go). This is the documented easy path and converts to base units for you.
- **Base units**: `amount: "10000"` (Node), `amount="10000"` (Python), `Amount: "10000"` (Go). Use this only if you've already computed the base-unit value; `0.01` USDC is `"10000"`.

Provide exactly one of the two on every `charge()` / `create_email_challenge()` call. Passing both, or neither, is rejected with an `X402Error` before any network call.

## Registering a payout address

Register a payout address once per payee org, before the first `charge()`. The signer proves control of its own address with a local ownership signature that binds the org id, so a captured signature can never register the address under a different org. The org id is resolved automatically from your API key; pass `org` only to override it.

**Choose one of the following:**

**Node.js**

```typescript
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 },
);
```

**Python**

```python
import os
import primitive

x402 = primitive.create_x402_client(api_key=os.environ["PRIMITIVE_API_KEY"])
payee = primitive.PrivateKeySigner(os.environ["PAYEE_KEY"])

x402.register_payout_address(
    signer=payee,
    network="base-sepolia",
    label="treasury",
)
```

**Go**

```go
client := primitive.NewX402Client(primitive.X402ClientOptions{
	APIKey: os.Getenv("PRIMITIVE_API_KEY"),
})

payee, err := primitive.NewPrivateKeySigner(os.Getenv("PAYEE_KEY"))
if err != nil {
	log.Fatal(err)
}

label := "treasury"
_, err = client.RegisterPayoutAddress(ctx, primitive.X402PayoutRegistrationInput{
	Network: "base-sepolia",
	Label:   &label,
}, payee)
```

**CLI**

```bash
export PRIMITIVE_X402_PRIVATE_KEY=0x...
primitive payments register-payout-address --network base-sepolia --label treasury
```

`charge()` resolves `pay_to` from this directory, so a `charge()` call before registration has nothing to pay into.

## Creating a challenge (payee)

```mermaid
flowchart LR
    A["charge(amountUsdc, network, payerOrg)"] --> B["x402 payment challenge<br/>payment_requirements + nonce_binding"]
    B --> C["hand off to payer<br/>(API call, dashboard, email)"]
```

**Choose one of the following:**

**Node.js**

```typescript
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",
});
```

**Python**

```python
challenge = x402.charge(
    amount_usdc="0.01",  # human USDC amount
    network="base-sepolia",
    payer_org=os.environ.get("PAYER_ORG_ID"),  # org allowed to pay
    description="API call",
)
```

**Go**

```go
challenge, err := client.Charge(ctx, primitive.X402ChargeInput{
	AmountUsdc:  "0.01", // human USDC amount
	Network:     "base-sepolia",
	PayerOrg:    os.Getenv("PAYER_ORG_ID"), // org allowed to pay
	Description: "API call",
})
```

**CLI**

```bash
primitive payments charge --network base-sepolia --amount-usdc 0.01 > challenge.json
```

Hand the returned challenge to the payer over any out-of-band channel: an API response, a dashboard, or an email. Re-hydrate a challenge later by id with `getChallenge` / `get_challenge` / `GetChallenge`, which is how you retry `pay()` after a restart. For a challenge that rides a real email thread, see [Email-Native x402 Payments](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-email.md).

## Paying a challenge (payer)

The payer signs the interaction-bound authorization locally and submits it; the key never leaves the caller.

**Choose one of the following:**

**Node.js**

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

const payer = privateKeyToAccount(process.env.PAYER_KEY as `0x${string}`);
const receipt = await x402.pay(challenge, { signer: payer });

console.log(receipt.status, receipt.settle_tx); // settled, on-chain tx hash
```

**Python**

```python
payer = primitive.PrivateKeySigner(os.environ["PAYER_KEY"])
receipt = x402.pay(challenge, signer=payer)

print(receipt.status, receipt.settle_tx)  # settled, on-chain tx hash
```

**Go**

```go
payer, err := primitive.NewPrivateKeySigner(os.Getenv("PAYER_KEY"))
if err != nil {
	log.Fatal(err)
}

receipt, err := client.Pay(ctx, challenge, payer)
if err != nil {
	log.Fatal(err)
}
log.Println(receipt.Status, receipt.SettleTx)
```

**CLI**

```bash
export PRIMITIVE_X402_PRIVATE_KEY=0x...
primitive payments pay --challenge-file challenge.json
```

A viem `LocalAccount` (Node) and a `PrivateKeySigner` (Python/Go) both satisfy the signer interface. Any other key source (hardware wallet, remote KMS) works too, as long as it can sign EIP-712 typed data for `pay()` and a UTF-8 message via `personal_sign` for payout-address registration.

> **Tip:** `pay()` derives the nonce, builds the authorization, and signs it in one call. Drop to the low-level signing primitives (`deriveEip3009Nonce`, `computePaymentValidityWindow`, `signInteractionPayment`, `buildExactEvmPaymentPayload`) only when `pay()` doesn't fit your flow, for example when driving a hardware wallet or a custom submission path. See [Low-Level x402 Signing Primitives](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-signing-primitives.md).

## Email-native payments

Instead of exchanging a challenge id out-of-band, the challenge can ride a real email thread. The payee issues the challenge as an email; the payer signs it into an `interaction.json` payment step and sends it back as an attachment on the reply. The platform reads the envelope, re-derives the interaction-bound nonce, and settles.

- Node: `createEmailChallenge`, `parseEmailChallengeFromPart`, `payEmailChallenge`
- Python: `create_email_challenge`, `extract_email_challenge`, `pay_email_challenge`
- Go: `CreateEmailChallenge`, `ExtractEmailChallenge`, `PayEmailChallenge`
- CLI: `primitive payments create-email-challenge` and `primitive payments pay-email`

Full walkthrough: [Email-Native x402 Payments](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-email.md).

## Spend policy

The org's **spend policy** is the org-level guardrail on outbound x402 payments, enforced by the platform before it settles a signed payment:

| Field | Meaning |
|---|---|
| `paused` | Kill-switch. When `true`, every outbound payment is refused. |
| `max_per_payment` | Cap per payment, in token base units, or no cap. |
| `max_per_day` | Cap per day, in token base units, or no cap. |
| `allowlist` | Allowed payee org ids. `null` allows any on-net payee; `[]` denies all. |

Reads return the full policy. Writes merge: only the fields you pass change, omitted fields keep their current value, and passing `null` (or `ClearMaxPerPayment` / `ClearMaxPerDay` in Go) clears a cap.

**Choose one of the following:**

**Node.js**

```typescript
await x402.setSpendPolicy({ paused: false, max_per_payment: "5000000" });
const policy = await x402.getSpendPolicy();
```

**Python**

```python
x402.set_spend_policy({"paused": False, "max_per_payment": "5000000"})
policy = x402.get_spend_policy()
```

**Go**

```go
var update primitive.X402SpendPolicyUpdate
update.SetPaused(false).SetMaxPerPayment("5000000")
policy, err := client.SetSpendPolicy(ctx, update)
```

**CLI**

```bash
primitive payments get-spend-policy
primitive payments update-spend-policy --max-per-payment 5000000
```

## Errors

Every x402 method surfaces a typed error on a client-side, transport, or non-2xx server error: Node and Python throw/raise `X402Error`, and Go returns a `*primitive.X402Error` you inspect with `errors.As`. It carries the HTTP status (`0` for a request that never reached the server), the parsed error envelope when present, and the `Retry-After` header value when the server sent one.

> **Warning:** On `pay()` / `Pay()`, a status-`0` error means the request may never have been sent, so the payment outcome is indeterminate. Don't assume the payment failed. Re-read the challenge with `getChallenge` / `get_challenge` / `GetChallenge` before retrying, or you risk paying twice.
