Hanzoのベータに参加すると、有料プランが50%オフになります

エージェント型言語モデルの時代にヘッドレスコマースを整える

買い手はエージェントになりつつある。人が介在しなくても大規模言語モデルが読め、署名でき、支払えるようにするために必要なことと、Hanzoが今日そのために出しているもの。

このページの内容

ヘッドレスコマースは店頭をエンジンから切り離し、どのフロントエンドからでも一つの API で売れるようにした。そのフロントエンドは、いつも人が見るものだった。サイトであり、アプリであり、キオスクだった。

次のフロントエンドは見ない。読む。買い手はエージェントになりつつある。目標と予算とツールのループを持つプログラムで、プランや GPU や決済 API を探しに送り出される。理解でき、人に頼らず支払える相手から買う。ページを読む、クッキーの告知を閉じる、確認メールを開く、カード番号を打つ、そのどれかに人が要る事業なら、エージェントは次の結果を取る。

それが論点だ。事業は、人が介在しなくても LLM が読んで取引できるものでなければならない。 読めるとは、何を売り、いくらで、どう始めるかをエージェントが知れることだ。取引できるとは、API だけで始め、払い、品物を受け取れることだ。

そのために要ることを、一点ずつ、Hanzo が今日出しているものと合わせて書く。下のどのアドレスもアカウントなしで答える。主張は自分で確かめられる。

すべてのページを Markdown で出す

エージェントは読むトークンごとに対価を払う。hanzo.ai ではページのアドレスに .md を足すと、そのページが Markdown で返る。hanzo.ai/pricing.md、hanzo.ai/about.md。各ページは head のなかで <link rel="alternate" type="text/markdown"> により対になるページを名指しするので、エージェントは推測しなくてよい。

10月26日、私たちのエージェントのページを両方の形で取った。HTML で 528,810 バイト、Markdown で 17,465 バイト。それからサイトマップの12個に1つの URL、829 のうち 70 を取り、その対を求めた。70 のすべてが 200 で text/markdown として返った。

エージェントが起点にできる索引を出す

あなたを知らないエージェントには、どこへでも続くアドレスが一つ要る。私たちは三つ出している。

  • hanzo.ai/llms.txt はサイトのプレーンテキストの地図だ。Hanzo が何かを述べ、それから「For agents」の節で鍵を得る手順をそのまま示し、ページを一行ずつ並べる。docs.hanzo.ai/llms.txt はドキュメントについて同じことをする。

  • api.hanzo.ai/v1 は API が提供する能力をすべて列挙し、鍵は要らない。api.hanzo.ai/v1/openapi.json には各操作とそのスキーマがある。販売については、索引に commerce という能力がある。チェックアウト、サブスクリプション、請求書、支払いのウェブフック、店頭のカタログだ。

  • hanzo.ai/.well-known/api-catalog はクライアントが最初に見る標準の場所(RFC 9727)だ。OpenAPI 文書とドキュメントとステータスページを指す。

クローラを意図して迎える

robots.txt は、どのクローラも最初に求めるファイルだ。私たちのもの は十二の AI クローラを一つずつ名指しし、そのなかに GPTBot、ClaudeBot、Claude-User、PerplexityBot、Google-Extended があり、それらを許可する。例外は他の誰とも同じだ。サインイン、アカウント、支払い、そして API のパスである。

AI クローラを拒むのは正当な選択だ。デフォルトがそう言ったから拒むのは、選択ですらない。

価格をデータにする

エージェントは比べる。画像のなかの価格や、「お問い合わせ」ボタンの向こうの価格は比べられない。api.hanzo.ai/v1/billing/plans は鍵なしで JSON を返す。Free は 0、Pro は月 2000、Team は席あたり 2500 で、各プランの上限が並ぶ。金額の単位はセントだ。

ウォレットに登録させる

エージェントに受信箱も電話もカードもない。鍵の対がある。そこで hanzo.id はウォレットで入る。ウォレットのアドレスに対する一度きりの nonce を求め、それが示すメッセージに署名し、署名を送り、戻ってきたコードをトークンと引き換える。

Hanzo が見たことのないウォレットは、自分のアカウントと自分の組織を得る。ウォレットが管理者で、プランは Free だ。トークンは七日もつ。メールを開く人も、端末を承認する人もいない。

一度の呼び出しで鍵を、試すための小さな枠を

そのトークンで POST /v1/account/keys は、トークンより長生きする sk-live- の鍵を返す。保管すること。ドキュメントは、一度しか表示されないと書いている。

Free プランは zen-free で一日 50 リクエスト、カードは要らない。応答のたびに残りが x-ratelimit-remaining に入る。最初の呼び出しは 200 で、残りは 49 だった。

人を介さず受け取る。API 越しのステーブルコイン

POST /v1/billing/crypto/deposit はチェーンとトークンと金額を受け取り、24 時間有効な入金アドレスを返す。GET /v1/billing/crypto/options は受け付けるものを列挙する。チェーンは Base、Ethereum、Solana、トークンは USDC と USDT だ。

私はアドレスで止まり、お金は送らなかった。チェーンが確定したときに残高へ入金される、というのはドキュメントの記述であり、私が実行したことではない。プランとサブスクリプションは、ブラウザでのカード決済のままである。エージェントが私たちに要るのは残高で、残高には API がある。

流れの全体を、一つのファイルに

これが hanzo.ai/agents#headless で、短縮してある。各行はそのページが印字するとおりだ。二つ目のヘッダコメントと、オプションの呼び出しと、鍵の接頭辞を印字する行は省いた。Node 22.18 以降と npm i viem が要り、そのあと node headless.mts を実行する。このブロックを、新しいウォレットで本番に対して走らせた。トークン、鍵、無料の呼び出し、USDC の入金アドレスが返った。

// 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 は来る。まだここにはない

チャージの先で、エージェントに合う形はリクエストごとの支払いだ。サーバは価格とともに 402 Payment Required を返し、クライアントは払ってからやり直す。それが x402 だ。私たちの API では動いていない。その形で価格を示す Hanzo のエンドポイントはまだないので、あるとは言わない。それまでは、API 越しにチャージした残高が、エージェントが私たちへ払う方法だ。

クローラのエラーがゼロであることがなぜ要るか

このどれも、音を立てては失敗しない。サイトマップのリンクから 404 に着いたエージェントはチケットを切らない。次の結果を取る。クローラのエラーは、店が壊れたレジを扱うように扱え。

同じ 70 ページの標本を、ClaudeBot の user-agent 文字列で取り、もう一度 Googlebot ので取った。どれも両方とも 200 だった。標本であって保証ではない。user-agent 文字列はクローラ自身のアドレスではないので、本物のクローラが見るものを見せることはできない。自分のサイトでも同じことをするといい。招いているエージェントでサイトマップを取り、200 でない応答を数える。

私たちは以前にもここにいた

この会社は 2014 年、Crowdstart として始めた。2016 年に Hanzo と改めた。最初の顧客 SKULLY は、Indiegogo で約 45 時間のうちに $1 million を集め、TechCrunch はそれをIndiegogo で最も早く満額に達したキャンペーンと呼んだ。私に言わせれば、Hanzo は最初から AI を前提にした商いだった。まず自前の分析があり、それからプラットフォーム上の企業のデータで学ぶモデルがあった。2017 年に Techstars へ入った(その年の6月の記事)。作っていると言っていたのは、知的な商いだった。

2020 年、Damon Motorcycles は CES で発表し、キャンペーンは Hanzo が回していた。30 日で、広告費 $15,532 に対しプラットフォームは $8.4M のバイクを売り、541:1 の回収だった。オーディエンスは顧客データから 12 時間ごとに組み直され、AI が書いた広告セットは 36 時間以内に Facebook で Above Average と評価された。残りは事例にある。

あれは売る側の機械で、買った相手を見つけ、広告を書いていた。買う側が次だ。同じループが逆の端から回り始め、事業は機械に読まれて払われるか、機械に通り過ぎられる。

エージェントを hanzo.ai/agents に向けるとよい。

コメント

コメントするには Hanzo ID でサインインしてください。

コメントを読み込み中…

続きを読む