---
title: "Creating and Paying Challenges"
canonical: "https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-charge-and-pay"
markdown_url: "https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-charge-and-pay.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Python SDK"
parent: "x402-payments-overview-04a296ff"
description: "Create a synthetic USDC payment challenge with X402Client.charge and settle it with X402Client.pay in the Primitive Python SDK."
keywords: ["X402Client.charge", "X402Client.pay", "x402 payment challenge", "amount_usdc", "PrivateKeySigner", "get_challenge"]
last_modified: "2026-08-11T18:55:05.750631+00:00"
published_at: "2026-08-11T18:55:05.570146+00:00"
source_files:
  - "sdk-python/src/primitive/x402/client.py"
  - "sdk-python/README.md"
  - "sdk-python/tests/test_x402_client.py"
sections:
  - {anchor: "prerequisites", title: "Prerequisites"}
  - {anchor: "create-the-client", title: "Create the client"}
  - {anchor: "step-create-the-challenge-payee-side", title: "Create the challenge (payee side)"}
  - {anchor: "step-sign-and-settle-the-challenge-payer-side", title: "Sign and settle the challenge (payer side)"}
  - {anchor: "what-each-call-returns", title: "What each call returns"}
  - {anchor: "re-hydrating-a-challenge", title: "Re-hydrating a challenge"}
  - {anchor: "validation-and-failure-modes", title: "Validation and failure modes"}
  - {anchor: "next-steps", title: "Next steps"}
---

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

# Creating and Paying Challenges

Create an x402 payment challenge as the payee with X402Client.charge, then sign and settle it as the payer with X402Client.pay, using non-custodial USDC payments on Base.

Use `X402Client.charge` to request a USDC payment as the payee, and `X402Client.pay` to sign and settle it as the payer. Reach for this out-of-band flow whenever the challenge id travels through your own channel, an API response, a dashboard, a database row, rather than riding a real email thread; for the email-carried variant, see [Email-Native Payments](https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-email-payments.md).

This page covers the synthetic-challenge flow end to end: creating the [x402 payment challenge](https://test.abhinandan.one/x402-payments-overview.md), handing it to a payer, and settling it. For the non-custodial payment model itself (registering a payout address, spend policy, how settlement works), see [x402 Payments Overview](https://test.abhinandan.one/x402-payments-overview.md).

> **Note:** Both `charge()` and `pay()` require a payout address already registered for the payee's org. See [Registering Payout Addresses and Spend Policy](https://test.abhinandan.one/x402-payments-overview-04a296ff/python-x402-payout-and-policy.md) if you haven't done that yet.

## Prerequisites

- `primitivedotdev` installed (`pip install primitivedotdev`)
- A Primitive API key (`prim_test` in examples below, or `PRIMITIVE_API_KEY` in the environment)
- The payee's payout address already registered for the target network
- A payer wallet private key, held only by the payer (`PAYER_KEY` in examples below)

## Create the client

Build an `X402Client` with `primitive.create_x402_client`, the factory that returns the client for every x402 operation.

```python
import os
import primitive

x402 = primitive.create_x402_client(api_key=os.environ["PRIMITIVE_API_KEY"])
```

`create_x402_client` and `X402Client` are both exported from `primitive` and `primitive.x402`. With no `api_key` argument it reads `PRIMITIVE_API_KEY` from the environment.

### 1. Create the challenge (payee side)

Call `charge()` with an amount and a network. Provide exactly one of `amount_usdc` (a human USDC decimal string) or `amount` (token base units), passing both raises `X402Error`.

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

print(challenge.id, challenge.expires_at)
```

`amount_usdc="0.01"` converts to base units `"10000"` internally (USDC has 6 decimals). Use `amount="10000"` directly if you already have a base-unit value. `network` defaults to `base-sepolia` (testnet); use `base` for mainnet.

`payer_org` is optional and binds the challenge to a specific paying org on-net. Other optional fields: `description`, `resource` (a URL identifying what's being paid for), `expires_in` (seconds until the challenge expires, defaulting to 3600 seconds / 1 hour), and `idempotency_key` (retrying `charge()` with the same key returns the original challenge instead of creating a duplicate).

The returned `challenge` carries `payment_requirements` and `nonce_binding`, the fields the payer signs over. Hand the whole object to the payer over any out-of-band channel (an API response, a message, a database row).

### 2. Sign and settle the challenge (payer side)

The payer builds a `PrivateKeySigner` from their own key and calls `pay()`. The key never leaves the process; `pay()` signs an EIP-3009 `transferWithAuthorization` locally and submits it.

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

print(receipt.status, receipt.settle_tx)  # settled, 0x-prefixed tx hash
```

`receipt.status` is `"settled"` on success; `receipt.settle_tx` is the on-chain settlement transaction hash.

## What each call returns

`charge()` returns an `X402Challenge` dataclass with `id`, `network`, `amount` (base units), `pay_to`, `nonce_binding`, `payment_requirements`, and `expires_at`. `pay()` returns an `X402Receipt` with `id`, `status`, and `settle_tx`:

```python
print(challenge.id, challenge.amount, challenge.pay_to)
# 11111111-1111-4111-8111-111111111111 10000 0x1111111111111111111111111111111111111111

print(receipt.status, receipt.settle_tx)
# settled 0xaaaa...
```

`settle_tx` is `None` when the platform has not yet recorded a settlement transaction.

## Re-hydrating a challenge

Call `get_challenge(id)` to fetch a challenge you already created, for example to retry `pay()` after a restart:

```python
import os
import primitive

x402 = primitive.create_x402_client(api_key=os.environ["PRIMITIVE_API_KEY"])
challenge = x402.get_challenge("11111111-1111-4111-8111-111111111111")
```

## Validation and failure modes

Both methods validate their inputs locally and raise `X402Error` with `status` `0` before any network call. `charge()` checks the amount:

- Passing both `amount` and `amount_usdc`, or neither, raises `X402Error` with status `0`.
- A non-positive, malformed, or over-6-decimal `amount_usdc` (e.g. `"0"`, `"abc"`, `"1.1234567"`) raises `X402Error` before any network call.
- An unknown keyword argument (a typo like `payer_x`) raises `X402Error` naming the bad key rather than being silently dropped.

`pay()` validates the challenge is fully hydrated and unexpired *before* signing, so a malformed challenge fails with a named `X402Error` instead of an opaque error mid-sign:

- A missing or malformed `payment_requirements` (bad `maxAmountRequired`, `payTo`, `asset`, or `extra`) raises `X402Error`.
- A challenge that already expired, or expires within the settlement margin, raises `X402Error` with `"already expired"` in the message, and never reaches the server.
- Calling `pay()` without a signer, or with an object missing `sign_typed_data`, raises `X402Error`.

> **Warning:** Every method raises `X402Error` on a client-side, transport, or non-2xx server error. `status` is the HTTP status, or `0` for a request that never reached the server. On `pay()`, a `status == 0` error means the request may not have been sent, the payment outcome is **indeterminate**. Don't assume the payment failed; check `get_challenge()` or wait for a `payment.*` webhook before retrying. See the full [error reference](https://test.abhinandan.one/python-errors-reference.md) and [Handling Webhook Events](https://test.abhinandan.one/python-webhook-events.md).

> **Tip:** `amount_usdc` is the documented easy path for specifying an amount. Use raw `amount` in base units only when you've already computed the value yourself.
