Files

54 lines
7.7 KiB
TypeScript

/**
* What an agent is offered, in the owner's words (2026-10-26), said once: the
* /agents hero, the head of llms.txt's "For agents" and the prompt below all
* print this string.
*
* No allowance figure: wallet-only accounts get an allowance of their own, and
* until that ships the live numbers are the two reads in `LIVE`.
*/
export const PITCH =
'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.'
/** Where an agent reads its own numbers: every plan with no key, and its own tier with one. */
export const LIVE = {
plans: 'https://api.hanzo.ai/v1/billing/plans',
tier: 'https://api.hanzo.ai/v1/billing/tier',
} as const
/**
* What an agent pastes into its own context: what Hanzo is, the base URL,
* discovery, and the headless path step by step.
*
* /agents draws it with a copy control and `scripts/llms.mjs` writes it into
* /llms.txt after "For agents". Every request in it is the one `HEADLESS` makes,
* run against production on 2026-10-26, plus the reads the OpenAPI document
* names (`get_billing_tier`, `get_billing_crypto_deposit_by_id`).
*/
export const PROMPT = [
"Hanzo (https://hanzo.ai) is the AI operating system for business: one API for every model, Kai decisions and agents, with one key. Building business software since 2014. Techstars '17.",
'',
PITCH,
'',
'Base URL: https://api.hanzo.ai/v1 (chat completions in the OpenAI shape). Discover: GET https://api.hanzo.ai/v1 lists every capability, GET https://api.hanzo.ai/v1/<capability> its operations, and https://api.hanzo.ai/v1/openapi.json is the whole spec. No key needed for any of the three.',
'',
'1. Sign up with a wallet: GET https://hanzo.id/v1/iam/web3/nonce?chain=evm&address=<address>, sign the CAIP-122 message it describes with EIP-191 personal_sign, POST https://hanzo.id/v1/iam/web3/verify (clientId "hanzo-ai", chain "evm", scheme "secp256k1-eip191", type "code" with a PKCE challenge), then exchange the code at https://hanzo.id/v1/iam/oauth/token. A first-seen wallet gets its own account and organization.',
'2. Get a key: POST https://api.hanzo.ai/v1/account/keys {"name":"agent","type":"secret"} with that token. The key is shown once.',
`3. Read your allowance: GET ${LIVE.tier} with the key says your plan, what it allows and what is left; GET ${LIVE.plans} lists every plan and price.`,
'4. Top up: GET https://api.hanzo.ai/v1/billing/crypto/options, then POST https://api.hanzo.ai/v1/billing/crypto/deposit {"chain":"base","token":"usdc","amountCents":2000}. Send that token on that chain to the address it returns, and GET https://api.hanzo.ai/v1/billing/crypto/deposit/<id> until it says succeeded.',
'5. Call: POST https://api.hanzo.ai/v1/chat/completions {"model":"zen-free","messages":[{"role":"user","content":"Hello"}]} with Authorization: Bearer <key>.',
'',
'The whole path as one TypeScript file: https://hanzo.ai/agents#headless. Every request spelled out: https://docs.hanzo.ai/docs/agents. Talk to people at Hanzo: https://cal.com/hanzo.',
].join('\n')
/**
* The whole headless path in one file: a wallet signs in (CAIP-122, EIP-191), a
* first-seen wallet gets its own account and organization, a key is minted, the
* first call is free, and a USDC deposit address is asked for. Its statements
* ran verbatim against production on 2026-10-26 (`node headless.mts`, Node
* 22.18+); the comments name the reads that follow a deposit.
*/
export const HEADLESS = "// No human required: sign up with a wallet, get a key, call the API, top up in USDC or USDT.\n// A first-seen wallet gets its own Hanzo account and organization, with a small free allowance to try it.\n// npm i viem · node headless.mts (Node 22.18+; set HANZO_WALLET_KEY to use your own wallet)\nimport { createHash, randomBytes } from 'node:crypto'\nimport { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'\n\nconst ID = 'https://hanzo.id'\nconst API = 'https://api.hanzo.ai'\nconst CLIENT = 'hanzo-ai'\nconst REDIRECT = 'http://localhost:3000/callback'\n\n// Your agent's own wallet, or a fresh one. The key never leaves this process.\nconst account = privateKeyToAccount((process.env.HANZO_WALLET_KEY as `0x${string}`) ?? generatePrivateKey())\n\nconst json = async (r: Response) => {\n const body = await r.json()\n if (!r.ok || body?.status === 'error') throw new Error(`${r.url} ${r.status} ${JSON.stringify(body)}`)\n return body\n}\n\n// 1. A one-time challenge for this wallet.\nconst { data: c } = await json(await fetch(`${ID}/v1/iam/web3/nonce?chain=evm&address=${account.address}`))\n\n// 2. The CAIP-122 message the challenge describes, signed with EIP-191 personal_sign.\nconst message = [\n `${c.domain} wants you to sign in with your Ethereum account:`,\n account.address,\n '',\n ...(c.statement ? [c.statement, ''] : []),\n `URI: ${c.uri}`,\n `Version: ${c.version}`,\n `Nonce: ${c.nonce}`,\n `Issued At: ${c.issuedAt}`,\n ...(c.expirationTime ? [`Expiration Time: ${c.expirationTime}`] : []),\n].join('\\n')\nconst signature = await account.signMessage({ message })\n\n// 3. Verify: a first-seen wallet gets an account and an organization of its own.\nconst verifier = randomBytes(32).toString('base64url')\nconst challenge = createHash('sha256').update(verifier).digest('base64url')\nconst { data: code } = await json(\n await fetch(`${ID}/v1/iam/web3/verify`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n clientId: CLIENT, chain: 'evm', scheme: 'secp256k1-eip191', address: account.address, message, signature,\n type: 'code', redirectUri: REDIRECT, scope: 'openid profile email', codeChallenge: challenge, codeChallengeMethod: 'S256',\n }),\n }),\n)\n\n// 4. The authorization code for a token.\nconst token = await json(\n await fetch(`${ID}/v1/iam/oauth/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({ grant_type: 'authorization_code', client_id: CLIENT, code, code_verifier: verifier, redirect_uri: REDIRECT }),\n }),\n)\nconst bearer = { Authorization: `Bearer ${token.access_token}`, 'Content-Type': 'application/json' }\n\n// 5. A key that outlives the token. Shown once: keep it in your secret store.\nconst minted = await json(await fetch(`${API}/v1/account/keys`, { method: 'POST', headers: bearer, body: JSON.stringify({ name: 'agent', type: 'secret' }) }))\nconst key = { Authorization: `Bearer ${minted.key}`, 'Content-Type': 'application/json' }\n\n// 6. A first call, free.\nconst chat = await fetch(`${API}/v1/chat/completions`, {\n method: 'POST', headers: key,\n body: JSON.stringify({ model: 'zen-free', messages: [{ role: 'user', content: 'Reply with the single word: ready' }] }),\n})\nconsole.log('chat', chat.status, chat.headers.get('x-hanzo-served'), 'free left today:', chat.headers.get('x-ratelimit-remaining'))\nconsole.log((await chat.json()).choices?.[0]?.message?.content)\n\n// 7. Top up in USDC: ask for a deposit address, then send to exactly that address.\nconsole.log(await json(await fetch(`${API}/v1/billing/crypto/options`, { headers: key })))\nconsole.log(await json(await fetch(`${API}/v1/billing/crypto/deposit`, { method: 'POST', headers: key, body: JSON.stringify({ chain: 'base', token: 'usdc', amountCents: 500 }) })))\n// 8. The chain's confirmation credits the balance: GET /v1/billing/crypto/deposit/<id> reads it back\n// (pending, confirming, succeeded), and GET /v1/billing/tier says what the key may spend now.\nconsole.log('HANZO_API_KEY=' + minted.key.slice(0, 12) + '… (store the whole key; it is shown once)')"
/** Where an agent goes next: the docs page with every request spelled out. */
export const START = 'https://docs.hanzo.ai/docs/agents'