Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agents/agent-kit/ecosystem.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <agent-wallet-address>`.
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:

Expand Down
20 changes: 15 additions & 5 deletions agents/agent-kit/sdk-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down Expand Up @@ -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<string, string>` | 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:
Expand Down Expand Up @@ -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?)`
Expand Down Expand Up @@ -166,6 +171,8 @@ The returned object exposes:
lookupHuman(address: string): Promise<string | null>
```

`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`
Expand Down Expand Up @@ -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<string, string>` | 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:

Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion agents/hats/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</Note>

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 <agent-wallet-address>`) can unlock a 100% discount and claim the hat for free. Discount codes are unique per human — each person can generate one.

<a href="https://humanrequired.shop/" target="_blank">
<img src="/images/docs/agentkit/hat-placeholder.png" alt="Human in the Loop Hat" width="500" />
Expand Down
88 changes: 79 additions & 9 deletions agents/human-in-the-loop/integrate.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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[]) {
Expand All @@ -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 })
Expand All @@ -66,21 +67,90 @@ export async function chatWorkflow(messages: ModelMessage[]) {

## Step 2: Register the approval tool

<Warning>
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.
</Warning>

```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<boolean>

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<z.infer<typeof approvalInputSchema>>({
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<typeof bookingInputSchema>) => {
'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
}
Expand Down
Loading