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.
Overview
Atlanticβs POST /atlantic-query endpoint speaks the canonical
x402 v2 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
instead. For AI agents, see the
atlantic-api AI skill.
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
# For anonymous flow, omit the -H flag entirely.
curl -X POST https://atlantic.api.herodotus.cloud/atlantic-query \
-H "x-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/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:
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.
Step 4 β Retry with PAYMENT-SIGNATURE
Build a v2 PaymentPayload and base64-encode it as the
PAYMENT-SIGNATURE header. Resubmit the same body:
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 ? { 'x-api-key': API_KEY } : {}),
'PAYMENT-SIGNATURE': headerValue,
},
body: formData,
});
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/1.1 200 OK
PAYMENT-RESPONSE: eyJ4NDAyVmVyc2lvbiI6Miwi...
{ "atlanticQueryId": "01HXX...", ... }
Decoded PAYMENT-RESPONSE:
{
"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 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:
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. 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:
/herodotus-skills:atlantic-api