Skip to main content

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 flowAnonymous flow
When it triggersAPI key + insufficient project creditsNo API key on the request
IdentityProject + API key (existing)EVM wallet recovered from EIP-3009 signature
Credit residueSettled amount β†’ project credit balance, 2-year TTL, drawn down by future queriesNone β€” pay-once, use-once
Refunds / leftoverYes, leftover credits remain on projectNo
dedupId / bucketIdSupportedRejected (WALLET_FLOW_DEDUP_ID_NOT_SUPPORTED, WALLET_FLOW_BUCKET_NOT_SUPPORTED)
Resumable after disconnectYes, via extra.atlantic_query_idYes, 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:
FieldWherePurpose
nameboth flowsEIP-712 domain name of the asset contract (e.g. "USDC"). Use this β€” do not call eip712Domain() on the contract.
versionboth flowsEIP-712 domain version of the asset contract (e.g. "2").
creditAmountboth flowsNumber of credits this payment will buy. Usually equals amount in cents.
challengeIdboth flowsServer-issued ULID. Single-use β€” the server invalidates it after a successful settle.
atlantic_query_idboth 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, clientIdAPI-key flow onlyIdentity 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)StatusWhen it firesWhat to do
X402_NOT_ENABLED503x402 disabled in this Atlantic deploymentBack off; do not retry
MISSING_API_KEY400Anonymous flow disabled and no API key suppliedAuthenticate with herodotus-auth and use the API-key flow
X402_CHALLENGE_FAILED502Upstream challenge build failedTransient β€” retry with backoff
X402_SETTLEMENT_FAILED402Verify 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_INVALID502Settlement succeeded on-chain but the response failed schema validationTreat as ambiguous β€” call GET /atlantic-query/:id before paying again to avoid double-charging
X402_SERVICE_AUTH_NOT_CONFIGURED500Server misconfiguration with the x402 facilitatorSurface to the user; do not retry
WALLET_FLOW_DEDUP_ID_NOT_SUPPORTED400Anonymous flow + dedupId in bodyDrop dedupId and resubmit
WALLET_FLOW_BUCKET_NOT_SUPPORTED400Anonymous flow + bucketId in bodyDrop bucketId and resubmit
WALLET_FLOW_NOT_RETRIABLE400This anonymous query cannot be retriedSubmit 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