> ## Documentation Index
> Fetch the complete documentation index at: https://docs.herodotus.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# x402 Payments

> Pay-per-call access to Atlantic over the canonical x402 v2 HTTP-payments protocol — works with both API-key fallback and pure-wallet anonymous flows.

## Overview

Atlantic's `POST /atlantic-query` endpoint speaks the canonical
**[x402 v2](https://github.com/coinbase/x402)** HTTP-payments protocol.
Header names match the public spec exactly
(`PAYMENT-REQUIRED`, `PAYMENT-SIGNATURE`, `PAYMENT-RESPONSE`), so any
standard x402 client library — `x402-fetch`, `@coinbase/x402-axios`, or
your own — works against Atlantic without modification.

Use x402 when you want pay-per-call access to Atlantic from an EVM
wallet, either:

* as a **fallback** when an API-key project runs out of prepaid
  credits, or
* as a **fully anonymous** wallet-only client with no account on
  Herodotus Cloud.

If you want long-lived session-based access with prepaid credits, use
the [Atlantic API authentication flow](/documentation/authentication)
instead. For AI agents, see the
[`atlantic-api` AI skill](/skills/atlantic-api).

## Two flows at a glance

|                                   | API-key flow                                                                          | Anonymous flow                                                                      |
| --------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| **When it triggers**              | API key + insufficient project credits                                                | No API key on the request                                                           |
| **Identity**                      | Project + API key (existing)                                                          | EVM wallet recovered from EIP-3009 signature                                        |
| **Credit residue**                | Settled amount → project credit balance, **2-year TTL**, drawn down by future queries | None — pay-once, use-once                                                           |
| **Refunds / leftover**            | Yes, leftover credits remain on project                                               | No                                                                                  |
| **`dedupId` / `bucketId`**        | Supported                                                                             | Rejected (`WALLET_FLOW_DEDUP_ID_NOT_SUPPORTED`, `WALLET_FLOW_BUCKET_NOT_SUPPORTED`) |
| **Resumable after disconnect**    | Yes, via `extra.atlantic_query_id`                                                    | Yes, via `extra.atlantic_query_id`                                                  |
| **`paymentRequired.error` field** | `"insufficient_credits"`                                                              | `"payment_required"`                                                                |

## Wire-level walkthrough

### Step 1 — Submit the query normally

```bash theme={null}
# For anonymous flow, omit the -H flag entirely.
curl -X POST https://atlantic.api.herodotus.cloud/atlantic-query \
  -H "api-key: $ATLANTIC_API_KEY" \
  -F pieFile=@input.zip \
  -F layout=dynamic \
  -F declaredJobSize=S
```

If the API key has sufficient credits, the response is the usual
`200` with the query body. **Do not preemptively send a
`PAYMENT-SIGNATURE` header** — the server only invokes x402 after
detecting `INSUFFICIENT_CREDITS`, and a payment header sent with
credits available is wasted.

### Step 2 — Receive the 402

If credits are insufficient (or no API key was provided and anonymous
payments are enabled), the server responds:

```http theme={null}
HTTP/1.1 402 Payment Required
PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6Miwi...

{
  "paymentRequired": {
    "x402Version": 2,
    "error": "insufficient_credits",
    "accepts": [
      {
        "scheme": "exact",
        "network": "base-sepolia",
        "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
        "payTo": "0xAbC...",
        "amount": "1000000",
        "resource": "Atlantic Query - 01HXX... - S",
        "statement": "By signing, you confirm…",
        "mimeType": "application/json",
        "maxTimeoutSeconds": 600,
        "extra": {
          "name": "USDC",
          "version": "2",
          "projectId": "01HXX...",
          "clientId": "01HXX...",
          "creditAmount": 100,
          "challengeId": "01HXX...",
          "atlantic_query_id": "01HXX..."
        }
      }
    ]
  },
  "paymentRequiredHeader": "eyJ4NDAyVmVyc2lvbiI6Miwi..."
}
```

The `PAYMENT-REQUIRED` HTTP header carries the same payload, base64-
encoded, for compatibility with standard x402 clients. Either source
is canonical; the body form is shown here because it's easier to
inspect.

`extra` field reference:

| Field                   | Where                 | Purpose                                                                                                                                  |
| ----------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                  | both flows            | EIP-712 domain `name` of the asset contract (e.g. `"USDC"`). Use this — do **not** call `eip712Domain()` on the contract.                |
| `version`               | both flows            | EIP-712 domain `version` of the asset contract (e.g. `"2"`).                                                                             |
| `creditAmount`          | both flows            | Number of credits this payment will buy. Usually equals `amount` in cents.                                                               |
| `challengeId`           | both flows            | Server-issued ULID. Single-use — the server invalidates it after a successful settle.                                                    |
| `atlantic_query_id`     | both flows (when set) | Pre-issued query ID. If you echo this in the signed payment, retrying the request resumes the same query rather than creating a new one. |
| `projectId`, `clientId` | API-key flow only     | Identity binding — the settle endpoint rejects payments whose `extra` does not match the calling project.                                |

### Step 3 — Sign EIP-3009 `transferWithAuthorization`

Pick one entry from `accepts[]` (typically the first; production
deployments may offer multiple networks). Then sign EIP-712 typed-data
for `transferWithAuthorization` on the chosen `asset` contract:

```ts theme={null}
import { privateKeyToAccount } from 'viem/accounts';

const NETWORK_TO_CHAIN_ID = {
  'base': 8453,
  'base-sepolia': 84532,
  'ethereum': 1,
  'sepolia': 11155111,
} as const;

const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY!);
const requirement = challenge.accepts[0];

const validAfter = 0n;
const validBefore = BigInt(
  Math.floor(Date.now() / 1000) + (requirement.maxTimeoutSeconds ?? 300),
);
const nonce = `0x${[...crypto.getRandomValues(new Uint8Array(32))]
  .map((b) => b.toString(16).padStart(2, '0'))
  .join('')}` as `0x${string}`;

const signature = await account.signTypedData({
  domain: {
    name: requirement.extra.name,            // from challenge
    version: requirement.extra.version,      // from challenge
    chainId: NETWORK_TO_CHAIN_ID[requirement.network],
    verifyingContract: requirement.asset,
  },
  types: {
    TransferWithAuthorization: [
      { name: 'from',        type: 'address' },
      { name: 'to',          type: 'address' },
      { name: 'value',       type: 'uint256' },
      { name: 'validAfter',  type: 'uint256' },
      { name: 'validBefore', type: 'uint256' },
      { name: 'nonce',       type: 'bytes32' },
    ],
  },
  primaryType: 'TransferWithAuthorization',
  message: {
    from: account.address,
    to: requirement.payTo,
    value: BigInt(requirement.amount),
    validAfter,
    validBefore,
    nonce,
  },
});
```

The wallet must hold at least `value` units of the asset on the
specified network. For `base-sepolia` USDC, that's
[Circle's faucet](https://faucet.circle.com).

### Step 4 — Retry with `PAYMENT-SIGNATURE`

Build a v2 `PaymentPayload` and base64-encode it as the
`PAYMENT-SIGNATURE` header. Resubmit the same body:

```ts theme={null}
const paymentPayload = {
  x402Version: 2,
  accepted: requirement,                       // verbatim, including extra
  payload: {
    signature,
    authorization: {
      from: account.address,
      to: requirement.payTo,
      value: requirement.amount,               // string, atomic units
      validAfter: validAfter.toString(),
      validBefore: validBefore.toString(),
      nonce,
    },
  },
};

const headerValue = Buffer.from(JSON.stringify(paymentPayload), 'utf8')
  .toString('base64');

const res = await fetch('https://atlantic.api.herodotus.cloud/atlantic-query', {
  method: 'POST',
  headers: {
    ...(API_KEY ? { 'api-key': API_KEY } : {}),
    'PAYMENT-SIGNATURE': headerValue,
  },
  body: formData,
});

if (!res.ok) {
  const errorBody = await res.text();
  throw new Error(`x402 retry failed with ${res.status}: ${errorBody}`);
}
```

### Step 5 — Read `PAYMENT-RESPONSE`

On success, the response is `200` with the usual query body and a
settlement receipt in the `PAYMENT-RESPONSE` header:

```http theme={null}
HTTP/1.1 200 OK
PAYMENT-RESPONSE: eyJ4NDAyVmVyc2lvbiI6Miwi...

{ "atlanticQueryId": "01HXX...", ... }
```

Decoded `PAYMENT-RESPONSE`:

```json theme={null}
{
  "x402Version": 2,
  "success": true,
  "transaction": "0xfacd…",   // on-chain tx hash, or derived id
  "network": "base-sepolia",
  "payer": "0xPayer…",
  "alreadyProcessed": false
}
```

`alreadyProcessed: true` means the facilitator has already settled this
exact `(challengeId, signature)` pair before — the query is allowed
through but no new credit is added. **Do not retry the payment.** This
typically happens when the client times out after the server has
settled but before the response is read.

## Replay, dedup, and idempotency

* **Challenges are single-use.** The server stores each issued
  challenge with its `challengeId` and consumes it on a successful
  settle. Reusing the same `PAYMENT-SIGNATURE` after success returns
  `X402_SETTLEMENT_FAILED`. For each new query, fetch a fresh 402.
* **Settlements are idempotent on `providerPaymentId`.** Submitting
  the same signed payment twice in a tight loop will not double-charge
  — the second attempt returns the same `200` with
  `alreadyProcessed: true`.
* **Resume an in-flight query.** If you submit a query, get the 402,
  sign, but lose the connection before retrying, you can recover the
  pre-issued `extra.atlantic_query_id` from your decoded challenge,
  embed it in the signed `accepted.extra`, and retry. The server
  routes the payment to the original query rather than minting a new
  one.

## Error taxonomy

| Code (agent-visible)                 | Status | When it fires                                                                                                                                  | What to do                                                                                                                      |
| ------------------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `X402_NOT_ENABLED`                   | 503    | x402 disabled in this Atlantic deployment                                                                                                      | Back off; do not retry                                                                                                          |
| `MISSING_API_KEY`                    | 400    | Anonymous flow disabled and no API key supplied                                                                                                | Authenticate with [`herodotus-auth`](/documentation/authentication#programmatic-wallet-authentication) and use the API-key flow |
| `X402_CHALLENGE_FAILED`              | 502    | Upstream challenge build failed                                                                                                                | Transient — retry with backoff                                                                                                  |
| `X402_SETTLEMENT_FAILED`             | 402    | Verify or settle rejected (bad signature, replay, expired authorization, insufficient wallet balance, mismatched `extra.projectId`/`clientId`) | Fetch a fresh 402 and try again with a new nonce; do not reuse the prior signature                                              |
| `X402_SETTLEMENT_RESPONSE_INVALID`   | 502    | Settlement succeeded on-chain but the response failed schema validation                                                                        | Treat as ambiguous — call `GET /atlantic-query/:id` before paying again to avoid double-charging                                |
| `X402_SERVICE_AUTH_NOT_CONFIGURED`   | 500    | Server misconfiguration with the x402 facilitator                                                                                              | Surface to the user; do not retry                                                                                               |
| `WALLET_FLOW_DEDUP_ID_NOT_SUPPORTED` | 400    | Anonymous flow + `dedupId` in body                                                                                                             | Drop `dedupId` and resubmit                                                                                                     |
| `WALLET_FLOW_BUCKET_NOT_SUPPORTED`   | 400    | Anonymous flow + `bucketId` in body                                                                                                            | Drop `bucketId` and resubmit                                                                                                    |
| `WALLET_FLOW_NOT_RETRIABLE`          | 400    | This anonymous query cannot be retried                                                                                                         | Submit a fresh query                                                                                                            |

## curl-only recipe

For sanity-checking your environment before integrating a signer:

```bash theme={null}
ATLANTIC=https://atlantic.api.herodotus.cloud

# 1. Submit; capture the 402 challenge body.
curl -s -o challenge.json -w '%{http_code}' -X POST \
  "$ATLANTIC/atlantic-query" \
  -F pieFile=@input.zip -F layout=dynamic -F declaredJobSize=S
# → 402

# 2. Inspect the challenge.
jq '.paymentRequired.accepts[0]' challenge.json

# 3. Sign EIP-3009 out of band (cast / wallet / KMS) and assemble the
#    PaymentPayload JSON in payment.json. Then:
SIG_HEADER=$(base64 -w0 < payment.json)

# 4. Retry with the signature header.
curl -s -X POST "$ATLANTIC/atlantic-query" \
  -H "PAYMENT-SIGNATURE: $SIG_HEADER" \
  -F pieFile=@input.zip -F layout=dynamic -F declaredJobSize=S \
  -D headers.txt

# 5. Read the receipt.
grep -i 'PAYMENT-RESPONSE' headers.txt | cut -d' ' -f2- | base64 -d | jq
```

## AI agent integration

If you're integrating Atlantic from an AI assistant or autonomous
agent, load the [`atlantic-api` AI skill](/skills/atlantic-api). It
includes this entire flow as a copy-pasteable playbook with anti-
hallucination guardrails and a viem reference snippet that handles
both flows in one parameterized function.

In Claude Code:

```bash theme={null}
/herodotus-skills:atlantic-api
```
