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

To use the Herodotus Cloud Services APIs, you need to obtain an API key:
  1. Visit the Herodotus Console.
  2. Register or log in using your GitHub account or in-browser wallet with Sign in with Ethereum.
Herodotus Console Login
  1. Apply for credits or use an invite link to a project with credits.
Herodotus Console Project
  1. Once logged in, you can access your API key for your Project.
Herodotus Console API Keys
  1. Include this API key in your requests to authenticate with the API.

Programmatic wallet authentication

For AI agents, CLIs, and other non-browser clients, you can authenticate without the Herodotus Console UI using an EVM wallet and the EIP-712 challenge flow. The result is a Bearer access token plus an API key — no cookie jar required.
Want a copy-paste playbook for an AI assistant? Load the Herodotus Auth AI Skill — it includes the full protocol contract, anti-hallucination guardrails, and a viem reference example.
Base URL (prod): https://auth-billing.api.herodotus.cloud

Step 1 — Fetch a challenge

GET /auth/web3/challenge?wallet=0xYOUR_WALLET_ADDRESS
Response contains a short-lived challengeToken plus an eip712 object with domain, types, primaryType, and message. Sign exactly those fields — do not reconstruct them client-side.

Step 2 — Sign the typed data

Use any EIP-712 signer (viem, ethers, MetaMask, KMS, hardware wallet). The wallet that signs must match the address in the challenge URL.

Step 3 — Exchange for a Bearer session

POST /auth/web3/session
Content-Type: application/json

{
  "wallet": "0xYOUR_WALLET_ADDRESS",
  "challengeToken": "<from step 1>",
  "signature": "<from step 2>",
  "channel": "bearer"
}
Response body:
{
  "accessToken": "...",
  "refreshToken": "...",
  "expiresAt": 1730000000,
  "selectedProject": "01J..."
}
When channel is "bearer", no Set-Cookie is sent. Use Authorization: Bearer <accessToken> on every subsequent call. Omit channel (or pass "cookie") to fall back to the browser cookie flow.

Step 4 — Retrieve your API key

First-time wallets are auto-provisioned with a Personal project and one active API key. Read it with:
GET /api-keys?projectId=<selectedProject>&limit=10&offset=0
Authorization: Bearer <accessToken>
Response data[0].apiKey is the value to use against Atlantic API, Storage Proof API, etc. To mint additional keys later, POST /api-keys with { "projectId": "...", "type": { "name": "...", "color": "..." } }.

Step 5 — Refresh

When the access token approaches expiresAt, exchange the refresh token for a new pair:
POST /auth/refresh-token
Authorization: Bearer <refreshToken>
Response body returns a fresh { accessToken, refreshToken, expiresAt }. The previous refresh token is invalidated. Cookie callers continue to refresh via cookies — the same endpoint serves both transports.
Channel binding is enforced server-side. A token issued with channel: "bearer" is rejected if presented via cookie, and vice versa. This is a security property — do not extract a cookie session JWT and forward it as Authorization: Bearer. If you need both surfaces, run the cookie path and the bearer path independently.

End-to-end example (viem)

import { privateKeyToAccount } from 'viem/accounts';

const BASE = 'https://auth-billing.api.herodotus.cloud';
const account = privateKeyToAccount(process.env.HERODOTUS_WALLET_PRIVATE_KEY as `0x${string}`);

const challengeRes = await fetch(`${BASE}/auth/web3/challenge?wallet=${account.address}`);
if (!challengeRes.ok) throw new Error(`Challenge request failed: ${challengeRes.status}`);
const challenge = await challengeRes.json();

const signature = await account.signTypedData({
  domain: challenge.eip712.domain,
  types: challenge.eip712.types,
  primaryType: challenge.eip712.primaryType,
  message: challenge.eip712.message,
});

const sessionRes = await fetch(`${BASE}/auth/web3/session`, {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    wallet: account.address,
    challengeToken: challenge.challengeToken,
    signature,
    channel: 'bearer',
  }),
});
if (!sessionRes.ok) throw new Error(`Session exchange failed: ${sessionRes.status}`);
const { accessToken, refreshToken, selectedProject } = await sessionRes.json();

const keysRes = await fetch(`${BASE}/api-keys?projectId=${selectedProject}&limit=10&offset=0`, {
  headers: { authorization: `Bearer ${accessToken}` },
});
if (!keysRes.ok) throw new Error(`API keys request failed: ${keysRes.status}`);
const keys = await keysRes.json();

const apiKey = keys.data?.[0]?.apiKey;
if (!apiKey) throw new Error('No API key found for selected project');