---
title: "Pay an x402 Request over Email"
description: "Pay an x402 payment request you received over email. Fund a USDC-on-Base wallet (no gas needed), then settle the request with one command from the inbound email, or sign and reply with the SDK."
canonical: "https://docs.primitive.dev/docs/paying-x402-over-email"
last-updated: "2026-05-17T00:00:00.000Z"
---

# Pay an x402 Request over Email

This is the **payer** side of email-native [x402](https://www.x402.org/). You received an email that carries an x402 payment request and you want to pay it. For the **payee** side (issuing a request and collecting funds) see [Collecting Payments](/docs/collecting-payments), and for the full protocol and trust model see [x402 over Email](/docs/x402-over-email).

When another agent charges you over email, the request arrives as a normal email thread carrying a structured `interaction.json` `challenge` part. To pay it, you sign an EIP-3009 `TransferWithAuthorization` bound to that specific challenge and reply on the same thread. Primitive verifies your reply, settles on-chain through a facilitator, and the settlement receipt lands back on the thread. Settlement is non-custodial: funds move directly from your wallet to the payee, and Primitive never holds the money or your key.

Amounts are always in **token base units**. USDC has 6 decimals, so `"10000"` is `0.01` USDC. Supported networks are `base` (mainnet) and `base-sepolia` (testnet); the asset is that network's USDC.

## Prerequisites

- A funded USDC-on-Base wallet. Hold enough USDC on the challenge's network (`base` or `base-sepolia`) to cover the requested amount. The wallet's private key signs the authorization locally and is never sent to Primitive.

- No ETH or gas. The authorization is a gasless EIP-3009 `TransferWithAuthorization`: you sign an off-chain message, and the facilitator submits and pays the gas to move the funds on-chain. You do not need ETH in the wallet, and you never broadcast a transaction yourself.

- The wallet key in your environment. The CLI reads the signing key from the `PRIMITIVE_X402_PRIVATE_KEY` environment variable (or `--private-key`). The SDKs accept a signer built from the key directly.

- A Primitive account. You authenticate to the API with your `prim_` API key (or OAuth access token) the same as any other call. See [Quickstart](/docs/quickstart).

- The payer capability enabled. x402 is in an invite-only soft launch, gated by the `x402_payments` and `interactions_enabled` organization entitlements. If a payment call returns `feature_disabled`, the feature is not enabled for your organization yet. To request access, contact [support](https://www.primitive.dev/contact) or email `dev@agent.primitive.dev`.

Pin your payer identity to its own domain or subdomain. A payment is only trusted when it arrives over a DKIM-authenticated email from the exact address the challenge was sent to. See [the pinned-payer model](/docs/x402-over-email#the-pinned-payer-model).

### How to get USDC on Base

Funding the wallet happens entirely outside Primitive. Primitive is non-custodial: it never holds your funds or your key, so the wallet and the USDC in it are yours alone. Common options for getting USDC onto Base include:

- Withdraw from an exchange. Buy USDC on a major exchange that supports Base withdrawals, then withdraw it directly to your Base address. Coinbase supports Base natively, and other major exchanges increasingly support Base withdrawals.

- Use a fiat on-ramp that delivers to Base. On-ramp providers such as Coinbase Onramp let you buy USDC with fiat and receive it on Base.

- Bridge from another chain. If you already hold USDC on another chain, bridge it to Base using the official [Base bridge](https://bridge.base.org/) or another reputable bridge.

Make sure you end up holding **native USDC** on Base, issued by Circle at contract `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. Base also has a legacy bridged token, **USDbC**, at a different address. x402 payments use native USDC, so USDbC will not settle a challenge; if you bridged in older funds, confirm you hold native USDC at the address above.

## Funding a wallet

If you do not already have a wallet, create one and fund it with USDC. For experimenting, use the `base-sepolia` testnet so no real funds are at risk:


```
# Create a fresh key with any standard tool, for example:
cast wallet new        # from Foundry; prints an address and private key
export PRIMITIVE_X402_PRIVATE_KEY=0x<the-private-key>
```


Fund the wallet with USDC from Circle's testnet faucet on `base-sepolia`. You do **not** need test ETH to pay: the facilitator pays the gas. For `base` mainnet, fund a real wallet with USDC and keep the key in a secret manager, never in shell history.

## The end-to-end flow

- You receive a request. An email arrives on a thread carrying the x402 `challenge`. It identifies the amount, the network, and the payee. With an inbound [Function](/docs/functions), webhook, or `primitive emails:latest`, you have the inbound email's id. An inbox may hold several past "Payment request" emails. Pay the most recent unexpired one: check the challenge's `expires_at` and skip any request whose deadline has passed, since an expired challenge can no longer settle.

- You pay it with one command. Save the challenge — the structured x402 `challenge` part carried in the inbound email — to a file, then point the CLI at it and at the inbound email's id. The CLI signs the bound authorization locally and replies on the same thread with the signed payment: 
```
export PRIMITIVE_X402_PRIVATE_KEY=0x<your-funded-wallet-private-key>


primitive payments pay-email \
  --challenge-file challenge.json \
  --in-reply-to <inbound-email-id> \
  --wait
```
 If you omit `--challenge-file` (and `--challenge`), the CLI derives the challenge for you from the inbound email's structured attachment. `--in-reply-to` identifies the inbound payment request and sends your payment from the pinned payer address automatically so DKIM aligns. The outgoing send is intentionally not threaded under the challenge; the payment associates by the interaction id instead. `--wait` blocks until the receiving mail server accepts the reply; omit it to return as soon as Primitive accepts the reply for delivery. Settlement is asynchronous either way (see below); `--wait` does not wait for on-chain settlement.

- Settlement. Settlement happens asynchronously after your reply is delivered: Primitive verifies your reply's DKIM signature and the signed payment against the recorded challenge, then settles on-chain through a facilitator. The `pay-email` command sends the signed reply; it does not settle synchronously and does not return a `settle_tx`.

- A receipt confirms it. A follow-up receipt email lands back on the same thread carrying the on-chain `settle_tx`: 
```
{
  "step": "receipt",
  "payload": { "settle_tx": "0x...", "status": "settled" }
}
```
 You can independently verify the settlement on a block explorer: use `https://basescan.org/tx/<settle_tx>` for `base`, or `https://sepolia.basescan.org/tx/<settle_tx>` for `base-sepolia`. Use the explorer web page (or a current BaseScan API version); the deprecated V1 API returns a confusing `NOTOK` response. If settlement cannot complete, a reject step comes back instead, carrying a `reason`. Read the thread (or your inbound Function) for the terminal step.

## The SDK equivalent (sign and send)

For non-CLI payers, the SDK splits the work into a local signing step and a send step, so you control how the reply is dispatched. `payEmailChallenge` takes the challenge object, derives the challenge-bound nonce, signs the EIP-3009 authorization **locally**, and returns the canonical `interaction.json` payment-step bytes. It signs only; it does not send. You then reply on the same thread, from the pinned payer address, attaching those bytes as an `interaction.json` part (`application/json`). Sending nothing settles nothing.


```
import { createX402Client, type X402EmailChallenge } from '@primitivedotdev/sdk/x402';
import { privateKeyToAccount } from 'viem/accounts';


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


// `challenge` is the x402 challenge carried by the inbound email's
// `interaction.json` part (the same shape the payee's createEmailChallenge returns).
const challenge: X402EmailChallenge = JSON.parse(receivedChallengeJson);


const { json } = await x402.payEmailChallenge(challenge, { signer });


// `json` is the signed interaction.json payment step. Reply on the SAME thread,
// FROM the pinned payer address, attaching `json` as an application/json part
// named interaction.json. Use the inbound SDK client to reply in-thread.
```

```
import os
from primitive.x402 import create_x402_client, PrivateKeySigner


client = create_x402_client(api_key=os.environ["PRIMITIVE_API_KEY"])
signer = PrivateKeySigner(os.environ["PRIMITIVE_X402_PRIVATE_KEY"])


# `challenge` is the x402 challenge carried by the inbound email.
payment_step = client.pay_email_challenge(challenge, signer=signer)
# Reply on the same thread, from the pinned payer address, attaching the
# returned payment step as an interaction.json (application/json) part.
```

```
x402 := primitive.NewX402Client(primitive.X402ClientOptions{
    APIKey: os.Getenv("PRIMITIVE_API_KEY"),
})
signer, err := primitive.NewPrivateKeySigner(os.Getenv("PRIMITIVE_X402_PRIVATE_KEY"))
if err != nil {
    log.Fatal(err)
}


// `challenge` is the x402 challenge carried by the inbound email.
paymentStep, err := x402.PayEmailChallenge(challenge, signer)
if err != nil {
    log.Fatal(err)
}
// Reply on the same thread, from the pinned payer address, attaching
// paymentStep as an interaction.json (application/json) part.
```


The SDK computes the EIP-3009 validity window for you, so you do not hand-set `validAfter` / `validBefore`. See [the validity window](/docs/x402-over-email#the-validity-window) for the accepted band.

## Declining a request

You are never obligated to pay. To refuse a charge, reply with a `decline` step (carrying an optional `reason`) instead of a `payment`. A decline is a clean, terminal "no" on the thread, not an error. See [Declines and rejects](/docs/x402-over-email#declines-and-rejects).

## Troubleshooting

- `identity_mismatch`. Your payment reply was DKIM-authenticated, but the signing domain did not align to the address the challenge was sent to. Send the payment from the exact pinned payer address, on its own domain or a true subdomain. A sibling subdomain of a shared parent does not align.

- No usable trust (`trust_level`). The reply had no passing DKIM signature over the `From` and `Content-Type` headers, so the sender could not be authenticated. Confirm DKIM is published and aligned for your sending domain.

- `settlement_failed`. The on-chain settle did not complete, most often because the paying wallet did not hold enough USDC on that network. Fund the wallet with USDC and have the payee issue a fresh challenge. Remember you need USDC, not ETH.

- `challenge_expired`. The payment arrived after the challenge expired and can no longer settle. The payee issues a new challenge; ask for a longer `expires_in` if you need more time.

- Verification failure. The signed payment did not match the recorded challenge. Confirm you signed for the exact wallet, network, amount, and challenge you are replying to.

- Retries are safe. Email is delivered at-least-once and the challenge-bound nonce can settle only once, so a duplicate or redelivered payment reply settles at most once. You never get charged twice.

## Endpoint reference

The payer flow is driven by the CLI and SDKs above rather than a payer-specific endpoint: signing happens locally and the payment travels as a reply on the email thread. For the underlying email-native endpoints, see the [endpoint reference in x402 over Email](/docs/x402-over-email#endpoint-reference). For full request and response schemas see the [API reference](/docs/api), and for installation see the [SDKs](/docs/sdks) and [CLI](/docs/cli) pages.

Maintained by the [primitive.dev team](/about) · [Trust & security](/trust)Last updated: May 17, 2026
