Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /x402-payments-overview-04a296ff/python-x402-charge-and-pay.md.
Verified · 8/11/2026

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.

This page covers the synthetic-challenge flow end to end: creating the x402 payment challenge, 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.

Note

Both charge() and pay() require a payout address already registered for the payee's org. See Registering Payout Addresses and Spend Policy 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.

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. 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.

    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. 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.

    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:

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:

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 and Handling Webhook Events.

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.

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer