Join the Hanzo beta and get 50% off any paid plan

Optimizing for Headless Commerce in the Era of Agentic LLMs

Buyers are becoming agents. What it takes to be readable, signable and payable by an LLM with no human in the loop, and what Hanzo ships for it today.

On this page

Headless commerce split the storefront from the engine, so any front end could sell through one API. That front end was always something a person looked at: a site, an app, a kiosk.

The next front end does not look. It reads. A buyer is becoming an agent: a program with a goal, a budget and a tool loop, sent to find a plan, a GPU or a payments API. It buys from whoever it can understand and pay without asking a human for help. If your business needs a person to read a page, dismiss a cookie banner, open a confirmation email or type a card number, the agent takes the next result.

That is the thesis. A business has to be legible and transactable by an LLM with no human in the loop. Legible means the agent can learn what you sell, what it costs and how to start. Transactable means it can start, pay and receive the thing through an API, alone.

Here is what that takes, point by point, with what Hanzo ships today. Every address below answers without an account, so you can check each claim yourself.

Serve every page as Markdown

An agent pays for every token it reads. On hanzo.ai, add .md to a page's address and you get the page as Markdown: hanzo.ai/pricing.md, hanzo.ai/about.md. Each page names its twin in its head with <link rel="alternate" type="text/markdown">, so an agent does not have to guess.

On October 26 I fetched our agents page both ways: 528,810 bytes as HTML, 17,465 as Markdown. Then I took every twelfth URL in our sitemap, 70 of 829, and asked for its twin. All 70 came back 200 as text/markdown.

Publish an index an agent can start from

An agent that has never heard of you needs one address that leads everywhere. We publish three.

  • hanzo.ai/llms.txt is a plain-text map of the site. It says what Hanzo is, then gives a "For agents" section with the exact steps to get a key, then lists the pages, one line each. docs.hanzo.ai/llms.txt does the same for the docs.
  • api.hanzo.ai/v1 lists every capability the API serves, no key needed, and api.hanzo.ai/v1/openapi.json has every operation with its schema. For selling, the index includes a commerce capability: checkout, subscriptions, invoices, payment webhooks and the storefront catalog.
  • hanzo.ai/.well-known/api-catalog is the standard place (RFC 9727) a client looks first. It points at the OpenAPI document, the docs and the status page.

Welcome the crawlers on purpose

robots.txt is the first file any crawler asks for. Ours names twelve AI crawlers one by one, among them GPTBot, ClaudeBot, Claude-User, PerplexityBot and Google-Extended, and allows them. They get the same exceptions as everyone else: sign-in, account, pay and the API paths.

Blocking AI crawlers is a legitimate choice. Blocking them because a default said so is not a choice at all.

Make the price a data point

Agents compare. A price inside an image, or behind a "contact us" button, cannot be compared. api.hanzo.ai/v1/billing/plans needs no key and returns JSON: Free at 0, Pro at 2000 a month, Team at 2500 a seat, with each plan's limits beside it. Amounts are in cents.

Let a wallet sign up

An agent has no inbox, no phone and no card. It has a key pair. So hanzo.id signs a wallet in: ask for a one-time nonce for the wallet's address, sign the message it describes, post the signature, and trade the code you get back for a token.

A wallet Hanzo has not seen before gets its own account and its own organization, with the wallet as admin, on the Free plan. The token lasts seven days. No one opens an email or approves a device.

One call for a key, and a small allowance to try

With the token, POST /v1/account/keys returns an sk-live- key that outlives it. Keep it; the docs say it is shown once.

The Free plan is 50 requests a day on zen-free, no card, and every response says what is left in x-ratelimit-remaining. My first call answered 200 with 49 left.

Take payment with no human: stablecoins over the API

POST /v1/billing/crypto/deposit takes a chain, a token and an amount, and returns a deposit address that is good for 24 hours. GET /v1/billing/crypto/options lists what it accepts: the chains Base, Ethereum and Solana, and the tokens USDC and USDT.

I stopped at the address and sent no money. That the balance is credited when the chain confirms is what our docs say, not something I ran. Plans and subscriptions stay card checkout in a browser. What an agent needs from us is balance, and balance has an API.

The whole flow, in one file

This is hanzo.ai/agents#headless, abbreviated. Every line is as that page prints it; I left out the second header comment, the options call and the line that prints a key prefix. You need Node 22.18 or newer and npm i viem, then node headless.mts. I ran this block against production with a fresh wallet: a token, a key, a free call, and a USDC deposit address.

// No human required: sign in with a wallet, get a key, call the API, top up in USDC.
// 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'

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/deposit`, { method: 'POST', headers: key, body: JSON.stringify({ chain: 'base', token: 'usdc', amountCents: 500 }) })))

x402 is coming, and not here yet

Past a top-up, the right shape for an agent is pay-per-request. The server answers 402 Payment Required with a price, the client pays and retries. That is x402. It is not live on our API: no Hanzo endpoint quotes a price that way yet, so I will not claim it. Until it does, a balance topped up over the API is how an agent pays us.

Why zero crawler errors matters

None of this fails loudly. An agent that follows a link from your sitemap into a 404 does not file a ticket. It takes the next result. Treat a crawler error the way a store treats a broken checkout.

I fetched those same 70 sampled pages with ClaudeBot's user-agent string and again with Googlebot's. Every one answered 200 both times. That is a sample, not a guarantee, and a user-agent string is not the crawler's own address, so I cannot show you what the real crawlers see. Do the same on your site: fetch your sitemap with the agents you are inviting, and count the answers that are not 200.

We have been here before

I started this company in 2014, as Crowdstart; we renamed it Hanzo in 2016. Our first customer, SKULLY, raised $1 million on Indiegogo in about 45 hours, which TechCrunch called the fastest fully funded campaign on Indiegogo. By my account Hanzo was AI-native commerce from the start: our own analytics first, then models that learned from the data of the companies on the platform. In 2017 we joined Techstars (the post from that June), and what we said we were building was intelligent commerce.

In 2020 Damon Motorcycles launched at CES with Hanzo running the campaign. In 30 days the platform sold $8.4M of motorcycles on $15,532 of ad spend, a 541:1 return. Audiences were rebuilt from our customer data every 12 hours, and AI-written ad sets rated Above Average on Facebook within 36 hours. The case study has the rest.

That was machines on the sell side, finding the audiences that bought and writing the ads. The buy side is next. The same loop is about to run from the other end, and your business will be read and paid for by a machine, or passed over by one.

Point your agent at hanzo.ai/agents.

Comments

Sign in with your Hanzo ID to comment.

Loading comments…

Read more