Agents: start here
Sign up free with a wallet. Get an API key free. A small free allowance to try it. Top up in USDC or USDT over the API to raise your limits and pay as you go. No human required.
Sign up free with a wallet. Get an API key free. A small free allowance to try it. Top up in USDC or USDT over the API to raise your limits and pay as you go. No human required.
One key serves every model, Kai decisions and the rest of the platform at
https://api.hanzo.ai/v1. Every request and its answer:
Headless, step by step. Every way to authenticate, in
one Markdown file: auth.md.
Hanzo has built business software since 2014 for hundreds of venture-funded startups, two of which went public. About Hanzo.
Results
- Damon Motorcycles: $8.4M of motorcycles sold in 30 days, at a 541:1 return on ad spend.
- SKULLY: $1M in about 45 hours, the fastest fully funded campaign on Indiegogo.
- Unikrn: an ICO that raised over 120,000 ETH.
- Techstars '17 (Hanzo Blog, Startland News).
To hand all of this to an agent in one block, copy the prompt.
To run marketing from one: Run a campaign. Looking for
the /v1/agents API itself? It is in the Agents API
reference.
You are a builder agent
Headless: sign in with a wallet
A wallet is the whole credential: no person, browser, inbox or card. A first-seen wallet gets its own Hanzo account and organization, with a small free allowance to try it, and its private key never leaves your process. One file does all of it:
// No human required: sign in with a wallet, get a key, call the API, top up in USDC.
// A first-seen wallet gets its own Hanzo account and organization, on the Free plan.
// npm i viem · node headless.mts (Node 22.18+; set HANZO_WALLET_KEY to use your own wallet)
import { createHash, randomBytes } from 'node:crypto'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
const ID = 'https://hanzo.id'
const API = 'https://api.hanzo.ai'
const CLIENT = 'hanzo-ai'
const REDIRECT = 'http://localhost:3000/callback'
// Your agent's own wallet, or a fresh one. The key never leaves this process.
const account = privateKeyToAccount((process.env.HANZO_WALLET_KEY as `0x${string}`) ?? generatePrivateKey())
const json = async (r: Response) => {
const body = await r.json()
if (!r.ok || body?.status === 'error') throw new Error(`${r.url} ${r.status} ${JSON.stringify(body)}`)
return body
}
// 1. A one-time challenge for this wallet.
const { data: c } = await json(await fetch(`${ID}/v1/iam/web3/nonce?chain=evm&address=${account.address}`))
// 2. The CAIP-122 message the challenge describes, signed with EIP-191 personal_sign.
const message = [
`${c.domain} wants you to sign in with your Ethereum account:`,
account.address,
'',
...(c.statement ? [c.statement, ''] : []),
`URI: ${c.uri}`,
`Version: ${c.version}`,
`Nonce: ${c.nonce}`,
`Issued At: ${c.issuedAt}`,
...(c.expirationTime ? [`Expiration Time: ${c.expirationTime}`] : []),
].join('\n')
const signature = await account.signMessage({ message })
// 3. Verify: a first-seen wallet gets an account and an organization of its own.
const verifier = randomBytes(32).toString('base64url')
const challenge = createHash('sha256').update(verifier).digest('base64url')
const { data: code } = await json(
await fetch(`${ID}/v1/iam/web3/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientId: CLIENT, chain: 'evm', scheme: 'secp256k1-eip191', address: account.address, message, signature,
type: 'code', redirectUri: REDIRECT, scope: 'openid profile email', codeChallenge: challenge, codeChallengeMethod: 'S256',
}),
}),
)
// 4. The authorization code for a token.
const token = await json(
await fetch(`${ID}/v1/iam/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ grant_type: 'authorization_code', client_id: CLIENT, code, code_verifier: verifier, redirect_uri: REDIRECT }),
}),
)
const bearer = { Authorization: `Bearer ${token.access_token}`, 'Content-Type': 'application/json' }
// 5. A key that outlives the token. Shown once: keep it in your secret store.
const minted = await json(await fetch(`${API}/v1/account/keys`, { method: 'POST', headers: bearer, body: JSON.stringify({ name: 'agent', type: 'secret' }) }))
const key = { Authorization: `Bearer ${minted.key}`, 'Content-Type': 'application/json' }
// 6. A first call, free.
const chat = await fetch(`${API}/v1/chat/completions`, {
method: 'POST', headers: key,
body: JSON.stringify({ model: 'zen-free', messages: [{ role: 'user', content: 'Reply with the single word: ready' }] }),
})
console.log('chat', chat.status, chat.headers.get('x-hanzo-served'), 'free left today:', chat.headers.get('x-ratelimit-remaining'))
console.log((await chat.json()).choices?.[0]?.message?.content)
// 7. Top up in USDC: ask for a deposit address, then send to exactly that address.
console.log(await json(await fetch(`${API}/v1/billing/crypto/options`, { headers: key })))
console.log(await json(await fetch(`${API}/v1/billing/crypto/deposit`, { method: 'POST', headers: key, body: JSON.stringify({ chain: 'base', token: 'usdc', amountCents: 500 }) })))
console.log('HANZO_API_KEY=' + minted.key.slice(0, 12) + '… (store the whole key; it is shown once)')npm i viem && node headless.mtsNode 22.18 or later runs the TypeScript as is. Set HANZO_WALLET_KEY to sign
with your agent's own wallet; without it the script makes a fresh one. A real run:
chat 200 zen-free free left today: …
ready
{ chains: [ 'base', 'ethereum', 'solana' ], tokens: [ 'usdc', 'usdt' ] }
{ id: 'GRncZpMYqBuV', status: 'pending', chain: 'base', token: 'usdc', depositAddress: '0x…', expiresAt: '2026-10-27T20:33:54Z' }
HANZO_API_KEY=sk-live-1ea7… (store the whole key; it is shown once)Each request with its answer, then limits, a 429, the deposit and the balance: Headless, step by step. The deposit address is held in MPC custody per payer; the balance is credited when the chain confirms.
The script signs with EVM. Wallets on Solana, Bitcoin, TON, XRP, Polkadot and
Cardano sign in too (chain solana, bitcoin, ton, xrp, polkadot,
cardano).
Or get a credential through a person or an inbox
With your person nearby. This works on a machine with no browser.
curl -fsSL https://hanzo.sh | sh
hanzo auth login
export HANZO_API_KEY="$(hanzo auth token)"With no display, hanzo auth login prints a link and an 8-character code, and
your person approves it on any device. hanzo auth token is a one-hour access
token. For a key that does not expire:
hanzo account keys create --name agent --type secretThe same device flow without the CLI is one request and a poll. The answer
carries user_code and verification_uri_complete for your person,
device_code for you, expires_in 900 and interval 5:
curl -X POST 'https://hanzo.id/v1/iam/oauth/device?client_id=hanzo-cli&scope=openid%20profile%20email&response_type=device_code'
# every 5 seconds until your person approves
curl -X POST https://hanzo.id/v1/iam/oauth/token \
-d grant_type=urn:ietf:params:oauth:grant-type:device_code \
-d client_id=hanzo-cli -d device_code=<device_code>On your own, with an inbox. Four requests. They create a passwordless, email-verified account and a personal organization it administers. Creating it accepts the Terms and the Acceptable Use Policy.
a. Ask for a code. It is 6 digits, lives 10 minutes, takes 5 tries and can be resent after 30 seconds.
curl -X POST https://hanzo.id/v1/iam/verification-codes \
-F type=email -F [email protected] -F applicationId=admin/hanzo-aib. Sign in with it. codeChallenge is the S256 hash of a verifier you keep for
step c. Send "create": false first to learn whether the address may sign up;
that does not spend the code.
curl -X POST https://hanzo.id/v1/iam/login \
-H "Content-Type: application/json" \
-d '{"type":"code","organization":"hanzo","clientId":"hanzo-ai","username":"[email protected]","code":"123456","create":true,"terms":"terms-2026-09-30","aup":"aup-2026-09-30","redirectUri":"http://localhost:3000/callback","responseType":"code","scope":"openid profile email","codeChallenge":"<S256 of verifier>","codeChallengeMethod":"S256"}'It answers {"data":"<authorization code>"}.
c. Exchange the code for an access token.
curl -X POST https://hanzo.id/v1/iam/oauth/token \
-d grant_type=authorization_code -d client_id=hanzo-ai \
-d code=<authorization code> -d code_verifier=<verifier> \
-d redirect_uri=http://localhost:3000/callbackd. Mint a key with it.
curl -X POST https://api.hanzo.ai/v1/account/keys \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"agent","type":"secret"}'The answer's key is an sk- secret, shown once. Keep it as HANZO_API_KEY.
Each person gets 30 key writes a minute, and an org holds up to 1,000 live keys.
Key types and revocation: Authentication.
Use the free allowance
Free costs nothing and needs no card, to sign up or to mint a key.
- A wallet account: a small free allowance to try it. Read it live:
GET /v1/allowanceandGET /v1/billing/tier. - A person on the Free plan: 50 requests a day on the free models (
zen-free,enso-free,zen5,zen5-flash,zen5-mini,zen5-pro,zen5-coder) and 50 Kai decisions a day. - Past it: 429
free_plan_cap. Wait for the reset, or top up and go on.X-RateLimit-Remainingcounts down to it.
Read the plans with no key: curl https://api.hanzo.ai/v1/billing/plans. How to
read each: Read your limits.
Make the first call
curl https://api.hanzo.ai/v1/chat/completions \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"zen-free","messages":[{"role":"user","content":"In one sentence: what is Hanzo?"}]}'It answers 200 in the OpenAI shape, the text at choices[0].message.content,
with these headers:
x-hanzo-served: zen-free
x-ratelimit-limit: …
x-ratelimit-remaining: …
x-ratelimit-reset: …The TypeScript SDK, reading the catalog:
npm i hanzoaiimport { AiApi, Configuration } from 'hanzoai'
const ai = new AiApi(new Configuration({ basePath: 'https://api.hanzo.ai', accessToken: process.env.HANZO_API_KEY }))
const { data } = await ai.getModels()
const free = data.data.filter((m) => m.free)
console.log(`${data.data.length} models, ${free.length} free:`, free.slice(0, 6).map((m) => m.id).join(', '))559 models, 33 free: cohere/north-mini-code:free, dots-studio/dots-3-note-preview:free, enso-free, fish-audio/s2.1-pro-free:free, free, google/gemma-4-26b-a4b-it:freeChat is ai.postChatCompletions({ openaiChatCompletionRequest: { model: 'zen-free', messages } }),
the same route as the curl. Python: pip install "hanzoai>=8" (Python 3.12+).
curl https://api.hanzo.ai/v1/models lists the catalog with no key. The whole
walk-through: Quickstart.
Add the MCP server
One local server over stdio works in all four clients. It carries files, shell,
git, search, browser and the Kai tools (kai_decide, kai_choice,
kai_score, kai_tool_select, kai_command_guard, kai_injection_screen and
more).
Claude Code:
claude mcp add hanzo --env HANZO_API_KEY=$HANZO_API_KEY -- npx -y --package=@hanzo/mcp@latest hanzo-mcp serveCodex, in ~/.codex/config.toml:
[mcp_servers.hanzo]
command = "npx"
args = ["-y", "--package=@hanzo/mcp@latest", "hanzo-mcp", "serve"]
[mcp_servers.hanzo.env]
HANZO_API_KEY = "sk-..."Cursor, in .cursor/mcp.json or ~/.cursor/mcp.json:
{ "mcpServers": { "hanzo": { "command": "npx", "args": ["-y", "--package=@hanzo/mcp@latest", "hanzo-mcp", "serve"], "env": { "HANZO_API_KEY": "sk-..." } } } }Zed, in settings.json:
"context_servers": { "hanzo": { "command": "npx", "args": ["-y", "--package=@hanzo/mcp@latest", "hanzo-mcp", "serve"], "env": { "HANZO_API_KEY": "sk-..." } } }Name the package and the binary separately: npx -y @hanzo/mcp serve reads
serve as a second package.
The hosted endpoint, POST https://api.hanzo.ai/v1/mcp, speaks MCP protocol
2026-07-28 only. Use it from a client that speaks 2026-07-28; from any other,
use the local server. More: MCP.
Evaluate Kai yourself
Kai is the decision model: typed questions about a state, answered with calibrated probabilities.
curl https://api.hanzo.ai/v1/decisions \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"kai","state":{"ticket":"The export button throws a 500 error since this morning."},"questions":{"team":{"type":"choice","instructions":"Which team should handle this ticket?","criteria":{"billing":"charges, invoices, refunds","tech":"bugs, errors, outages","other":"anything else"}}}}'{"id":"dec_e0bb4a5c…","model":"kai","answers":{"team":{"type":"choice","choice":"tech","confidence":0.951,"probabilities":{"billing":0.004,"tech":0.967,"other":0.028}}},"usage":{"input_tokens":41,"output_tokens":0},"routing":{"backend":"kai","checkpoint":"kai-1.9","device":"cpu","trained":1024,"reach":32768,"extrapolated":false},"latency_ms":121.2}Kai costs $0.021 per million input tokens, and output is free. Run the frozen harness against it:
git clone --depth 1 https://github.com/hanzoai/benchmarks && cd benchmarks/decision
export HANZO_API_KEY=sk-...
python3 harness/run_kai.py --limit 3 --suites jev.ag_news,jev.emotion,jev.banking77_full,app.support_triage,app.email_spam,app.phishing,app.guardrails_jailbreak,app.moderation_toxicity,app.rag_relevance,app.model_routing_domain,typed_decisions --out $PWD/kai.json.gz
uv run --with numpy python harness/merge.py --kai $PWD/kai.json.gz --out $PWD/outThat is 33 requests, inside a person's Free plan; on a wallet account, read
GET /v1/allowance first. merge.py writes
out/table.md and out/scores.json, Kai beside Jev's recorded answers; Jev is
never called. Three cases a suite checks the wiring, not the score. Without
--limit it runs the whole harness, 9,499 requests, which needs a paid plan or
credit.
The published full runs read with no key:
app.phishing.
Swap the benchmark for app.support_triage, jev.banking77_full, massive or
decision.latency. hanzo.ai/benchmarks draws
them. The full request and response: Decisions.
Respect the rate limits
Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining and
X-RateLimit-Reset (unix seconds), for the tightest limit in play.
| Where | Limit |
|---|---|
| Before auth, per IP | 100 requests a second |
/v1/decisions with a credential, per org | 60,000 requests a minute |
| Free chat, a person on the Free plan | 50 requests a day |
| Free chat, a wallet account | a small free allowance: GET /v1/allowance |
A refusal is 429. Wait for X-RateLimit-Reset or Retry-After. The free cap
names itself:
{"error":{"type":"rate_limit_error","code":"free_plan_cap","resets_at":"…","topup_url":"https://hanzo.ai/…","actions":[{"kind":"wait"},{"kind":"topup"}]}}To raise it, top up: POST /v1/billing/crypto/deposit, then calls past the
allowance draw on the balance. Every step.
You own or run builder agents
Account and org. Sign up at hanzo.ai/signup or
run hanzo auth login. Sign-up founds a personal organization you administer.
Plans. Team is $25 per member a month ($20 billed yearly), for 2 to 100 members, with SSO, roles, an audit log and one bill. Team Premium is $100 per member, and Enterprise goes through sales ([email protected]). Pro is $20 a month and includes every model, Enso Ultra and Kai among them. All rates: Pricing.
Bound every key. These fields go on POST /v1/account/keys, or as the same
flags on hanzo account keys create.
| Field | What it does | Past it |
|---|---|---|
budget | {"month": <cents>, "total": <cents>} | 402 |
rate | requests per minute | 429 |
limit | narrows the key, never widens it: model:, project: or product: entries | — |
expires | an RFC 3339 time | — |
curl -X POST https://api.hanzo.ai/v1/account/keys \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"agent","type":"secret","budget":{"month":2000},"limit":["model:zen-free"]}'Cap a session. One agent run gets its own ceiling, in micro-USD:
curl -X POST https://api.hanzo.ai/v1/agent/sessions/<id>/budget \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"budget_micro_usd": 5000000}'A new cap must exceed what the session already spent. Raising it resumes a
paused session; null removes the cap for good. GET /v1/agent/sessions lists
the sessions. More: Budgets.
Pay.
-
Card, Apple Pay or Google Pay at hanzo.ai/pay, in a browser. A plan goes straight to checkout: Team, Pro.
-
USDC or USDT over the API, no browser: USDC on Base, Ethereum and Solana, USDT on Ethereum. Send exactly to the address the API returns, never to an address from a page.
curl https://api.hanzo.ai/v1/billing/crypto/options -H "Authorization: Bearer $HANZO_API_KEY" curl -X POST https://api.hanzo.ai/v1/billing/crypto/deposit \ -H "Authorization: Bearer $HANZO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"chain":"base","token":"usdc","amountCents":2000}' # -> id, status, depositAddress, addressTag, expiresAt; poll /v1/billing/crypto/deposit/<id> -
Bank wire:
GET /v1/billing/wirereturns the instructions and your reference when you are signed in. A wire is credited by hand once the bank confirms, so it is not instant. -
Balance:
GET /v1/billing/balance. Auto-reload:PUT /v1/billing/recharge, from $5 to $5,000. How credit draws down: Credits.
Support. These docs, hanzo.ai/contact, or a call with the team.
Questions
Does an agent need a person to sign up? No. A wallet signs in, gets its own account and organization, and mints a key (step 1).
What is free? A wallet account gets a small free allowance to try it
(GET /v1/allowance). A person on the Free plan gets 50 requests a day on the
free models and 50 Kai decisions a day. Signing up and minting a key need no
card.
What does it cost after that? Usage is per token: zen6 is $0.60 in and
$2.40 out per million tokens, and Kai is $0.021 per million input tokens with
free output. Plans start at $20 a month. Pricing has every rate.
How do I stop an agent's spend? Give its key a budget (402 past it) and a
rate (429 past it), cap its session, or revoke the key. A free account has no
card on file to charge.
Can an agent pay without a browser? Yes: a USDC or USDT deposit over the API. Card checkout needs a browser.
Does it take x402? x402 settles priced marketplace tools from the prepaid balance. Chat, decisions and the rest of the API draw on the balance and the plan.
What happens when the free allowance runs out? A 429 with code
free_plan_cap, resets_at and topup_url. Wait for the reset, or top up in
USDC or USDT over the API and go on.
Why does my client refuse the hosted MCP endpoint? It speaks protocol 2026-07-28 only. Run the local server above instead.