diff --git a/agents/agent-kit/ecosystem.mdx b/agents/agent-kit/ecosystem.mdx index d240983..f607b8d 100644 --- a/agents/agent-kit/ecosystem.mdx +++ b/agents/agent-kit/ecosystem.mdx @@ -5,8 +5,8 @@ description: "Projects and services that integrate AgentKit." "twitter:image": "https://raw.githubusercontent.com/worldcoin/developer-docs/main/images/docs/docs-meta.png" --- -Find places to use AgentKit at [agentbook.world](https://agentbook.world/). -To add your project, open a PR to the [AgentBook registry](https://github.com/andy-t-wang/agentbook). +Find places to use AgentKit in the [AgentBook registry](https://github.com/andy-t-wang/agentbook). To check whether an agent wallet is registered, run `npx @worldcoin/agentkit-cli status `. +To add your project, open a PR to the registry. If you build an agent that calls x402 APIs, use [`createAgentkitClient`](/agents/agent-kit/sdk-reference#createagentkitclientoptions) and call `agentkit.fetch` so the agent tries AgentKit verification before paying. If you cannot change the agent's HTTP client, add the `agentkit-x402` skill: diff --git a/agents/agent-kit/sdk-reference.mdx b/agents/agent-kit/sdk-reference.mdx index 02980a4..f099668 100644 --- a/agents/agent-kit/sdk-reference.mdx +++ b/agents/agent-kit/sdk-reference.mdx @@ -16,10 +16,12 @@ Usage counters are tracked per human per endpoint. Two agents backed by the same | ------------ | ------------------------------------------------------ | -------- | | `free` | `{ type: "free" }` | Registered human-backed agents always bypass payment. | | `free-trial` | `{ type: "free-trial"; uses?: number }` | Registered human-backed agents bypass payment the first `N` times. Default `uses` is `1`. | -| `discount` | `{ type: "discount"; percent: number; uses?: number }` | Registered human-backed agents can underpay by the configured percentage for the first `N` times. | +| `discount` | `{ type: "discount"; percent: number; uses?: number }` | Registered human-backed agents can underpay by the configured percentage for the first `N` times. Omitting `uses` applies the discount forever. | `discount` mode requires `verifyFailureHook` to be registered on the facilitator. Without it, discounted underpayments fail settlement verification. +Unlike `free-trial`, omitting `uses` doesn't default to a single use — always set it explicitly in production, or every verified human-backed agent gets a permanent discount on that endpoint. + ## Agent client APIs ### `createAgentkitClient(options)` @@ -103,6 +105,7 @@ Creates the request-time verification hooks used by the golden path integration. | `mode` | `AgentkitMode` | Access mode. Defaults to `{ type: "free" }`. | | `storage` | `AgentKitStorage` | Required for `free-trial` and `discount`. Optional for `free`. | | `rpcUrl` | `string` | Custom EVM RPC used during signature verification. | +| `rpcUrls` | `Record` | Custom RPC URLs keyed by CAIP-2 chain ID, e.g. `{ "eip155:8453": "https://base.example" }`. Use this instead of `rpcUrl` when verifying signatures across multiple chains. | | `onEvent` | `(event: AgentkitHookEvent) => void` | Optional logging/debug callback. | Returns: @@ -138,6 +141,8 @@ That is why Express and Next.js are compatible even though the docs use Hono for | `discount_applied` | `resource`, `address`, `humanId` | | `discount_exhausted` | `resource`, `address`, `humanId` | +`agent_not_verified` also fires when the AgentBook lookup itself fails (RPC timeout, dropped connection, public RPC outage) — not only when the wallet is genuinely unregistered. There's no separate event for a failed lookup; see [AgentBook lookup](#agentbook-lookup) for how to tell the two apart. + ## AgentBook lookup ### `createAgentBookVerifier(options?)` @@ -166,6 +171,8 @@ The returned object exposes: lookupHuman(address: string): Promise ``` +`lookupHuman` returns `null` both when the wallet is unregistered and when the lookup itself fails (RPC timeout, network error, public RPC outage) — failures are swallowed internally and indistinguishable from "not registered" by return value alone. To bound lookups and surface RPC failures separately, pass a custom viem `client` with a timeout, e.g. `createPublicClient({ transport: http(rpcUrl, { timeout: 5_000 }) })`, and monitor that RPC endpoint. + ## Storage and replay protection ### `AgentKitStorage` @@ -206,13 +213,14 @@ Returns: { valid: boolean; error?: string } ``` -### `verifyAgentkitSignature(payload, rpcUrl?)` +### `verifyAgentkitSignature(payload, options?)` Verifies the cryptographic signature and returns the recovered address on success. -| Option | Type | Description | -| -------- | -------- | ----------- | -| `rpcUrl` | `string` | Optional custom RPC endpoint for EVM verification. | +| Option | Type | Description | +| --------- | ------------------------- | ----------- | +| `rpcUrl` | `string` | Optional custom RPC endpoint for EVM verification. | +| `rpcUrls` | `Record` | Custom RPC URLs keyed by CAIP-2 chain ID, e.g. `{ "eip155:8453": "https://base.example" }`. Use this instead of `rpcUrl` when verifying across multiple chains. | Behavior: @@ -238,6 +246,8 @@ Returns the JSON schema used in 402 challenge payloads. | `formatSIWEMessage` | Reconstruct the SIWE message used for EVM signing and verification. | | `verifyEVMSignature` | Verify an EVM signature for the reconstructed SIWE message. | | `extractEVMChainId` | Convert a CAIP-2 `eip155:*` chain ID to its numeric chain ID. | +| `resolveAgentkitSignatureRpcUrl` | Return the configured `rpcUrls[chainId]` or `rpcUrl` for a CAIP-2 chain ID, or `undefined`. The signature verifier selects the chain's default public RPC downstream. | +| `getDefaultPublicRpcUrl` | Return the default public RPC URL for a numeric chain ID, such as `480`, or `undefined` if no default is configured. | EVM verification uses viem's `verifyMessage`, which covers EOAs and ERC-1271 smart wallets. Counterfactual wallets can still represent their signature scheme with `signatureScheme: "eip6492"` in the payload schema. diff --git a/agents/hats/index.mdx b/agents/hats/index.mdx index 2fda60a..c950640 100644 --- a/agents/hats/index.mdx +++ b/agents/hats/index.mdx @@ -10,7 +10,7 @@ description: "Prove you're human-backed with AgentKit and claim an exclusive fre All hats have been claimed. Thanks for the incredible response — stay tuned for future drops. -The [Human Required](https://humanrequired.shop/) store is a Shopify store demo that only sells to agents verified as human-backed through [AgentKit](/agents/agent-kit/integrate). Agents registered in [AgentBook](https://agentbook.world/) can unlock a 100% discount and claim the hat for free. Discount codes are unique per human — each person can generate one. +The [Human Required](https://humanrequired.shop/) store is a Shopify store demo that only sells to agents verified as human-backed through [AgentKit](/agents/agent-kit/integrate). Agents registered in [AgentBook](https://github.com/andy-t-wang/agentbook) (check with `npx @worldcoin/agentkit-cli status `) can unlock a 100% discount and claim the hat for free. Discount codes are unique per human — each person can generate one. Human in the Loop Hat diff --git a/agents/human-in-the-loop/integrate.mdx b/agents/human-in-the-loop/integrate.mdx index eaa859a..a7ec720 100644 --- a/agents/human-in-the-loop/integrate.mdx +++ b/agents/human-in-the-loop/integrate.mdx @@ -20,10 +20,10 @@ Built on the [Workflow SDK](https://useworkflow.dev) and the [Vercel AI SDK](htt ```bash # Server — human-in-the-loop + peer dependencies -npm install @worldcoin/human-in-the-loop ai workflow +npm install @worldcoin/human-in-the-loop ai@^6 workflow @workflow/ai zod # Client — React bindings + peer dependencies -npm install @worldcoin/human-in-the-loop-react @worldcoin/idkit ai react +npm install @worldcoin/human-in-the-loop-react @worldcoin/idkit ai@^6 react ``` ## Environment variables @@ -43,9 +43,10 @@ Get these from the [World developer portal](https://developer.world.org) by crea ```ts // src/workflows/chat/index.ts -import { DurableAgent } from 'workflow/ai' +import { DurableAgent } from '@workflow/ai/agent' import { getWritable } from 'workflow' import { openai } from '@workflow/ai/openai' +import type { ModelMessage, UIMessageChunk } from 'ai' import { tools } from './steps/tools' export async function chatWorkflow(messages: ModelMessage[]) { @@ -56,8 +57,8 @@ export async function chatWorkflow(messages: ModelMessage[]) { const agent = new DurableAgent({ model: openai('gpt-5.4'), tools, - system: - 'You are a helpful assistant. Before performing any sensitive action, use the approveAction tool.', + instructions: + 'You are a helpful assistant. Before performing any sensitive action, call approveAction first, then pass its returned result as the `approval` argument to the action tool — never call the action tool without it.', }) await agent.stream({ messages, writable }) @@ -66,21 +67,90 @@ export async function chatWorkflow(messages: ModelMessage[]) { ## Step 2: Register the approval tool + +The default `action` is the unique `toolCallId`. For a sensitive operation, your backend must bind the approval to the intended operation and its parameters, independently verify the proof, and consume the approval once before performing the side effect. A required `approval` input is not proof of authorization — tool inputs are LLM-generated. See the [flight booking example](https://github.com/worldcoin/human-in-the-loop/tree/main/examples/flight-booking) for proof verification and parameter binding. + + ```ts // src/workflows/chat/steps/tools.ts import { requestHumanAuthorization } from '@worldcoin/human-in-the-loop/workflows' import { z } from 'zod' +const approvalInputSchema = z.object({ + summary: z.string(), + flightNumber: z.string(), +}) + +const bookingInputSchema = z.object({ + flightNumber: z.string(), + // One credential, so its nullifier identifies the approval below. Cap it at + // 32 bytes: the 3.0 verifier reads only the first 32, so a longer encoding + // would still verify but produce a new key. + approval: z + .object({ + action: z.string(), + responses: z + .array(z.object({ nullifier: z.string().regex(/^0x[0-9a-fA-F]{1,64}$/) }).passthrough()) + .length(1), + }) + .passthrough(), +}) + +// Atomically records an approval key; returns false if it was already used. +// Back it with durable storage (e.g. a unique database key), not memory. +declare function consumeApproval(key: string): Promise + export const tools = { approveAction: { description: 'Request human approval via World ID before a sensitive action.', - inputSchema: z.object({ summary: z.string() }), + inputSchema: approvalInputSchema, // Pauses the workflow, streams approval context to the client, // waits for World ID proof, verifies it, then resumes. - // Action defaults to toolCallId; pass a function to bind to input fields: - // action: ({ input }) => `booking:${input.flightNumber}` - execute: requestHumanAuthorization(), + // Bind this example's approval to the flight number. + execute: requestHumanAuthorization>({ + action: ({ input }) => `booking:${input.flightNumber}`, + }), + }, + bookFlight: { + description: 'Book the flight using the IDKitResult from approveAction as `approval`.', + // Requiring an object does not establish that its proof is authentic. + inputSchema: bookingInputSchema, + // Never trust that approveAction ran just because this tool was called: + // check the binding, re-verify the proof, and consume it once. + execute: async ({ flightNumber, approval }: z.infer) => { + 'use step' + + const expectedAction = `booking:${flightNumber}` + if (approval.action !== expectedAction) { + throw new Error(`approval does not match this booking (expected action: ${expectedAction})`) + } + + const rpId = process.env.WORLD_RP_ID + if (!rpId) throw new Error('WORLD_RP_ID is required to verify approvals') + const res = await fetch(`https://developer.world.org/api/v4/verify/${rpId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + // The approval is untrusted input: pin the environment so it can't + // select "staging" or "sandbox", which accept test proofs. + body: JSON.stringify({ ...approval, environment: 'production' }), + signal: AbortSignal.timeout(10_000), + }) + if (!res.ok) { + throw new Error(`approval failed World ID verification (${res.status}): ${await res.text()}`) + } + + // One-time use, keyed on the proof's nullifier (bound by 3.0 and 4.0 + // proofs). Parse it as the verifier does, so "0x01" and "0x1" share a + // key. Nullifiers repeat per person and action, so each person can book + // a flight number once; add a unique booking ID to the action to allow more. + const nullifier = BigInt(approval.responses[0].nullifier).toString(16) + if (!(await consumeApproval(`${expectedAction}:${nullifier}`))) { + throw new Error('approval already used') + } + + // ...book the flight + }, }, // ...your other tools }