
Human Gate
Human Gate is verifiable authorization middleware for AI agents.
Demo
About
HumanGate is a verifiable authorization gate for agent tool calls: nothing commits until a human, verified with World ID at that moment, authorizes *that exact operation*. HumanGate sits between an agent's tool call and the operation it wants to perform. The agent holds the credential and does the waiting, the polling and the buying. Nothing commits until a real human — verified with World ID, present at that moment — authorizes that exact operation. The authorization is bound to the operation, re-verified on the server, and consumed exactly once. The middleware is the product. The ticket queue is the demo.
How it's made
Built in TypeScript on Next.js 15 App Router with synchronous SQLite (`better-sqlite3`), `openid-client` v6 for OIDC, and the official `@modelcontextprotocol/sdk` for a stdio MCP server. `worldid/` is the only module that touches World; `lib/gate.ts` holds the single function the HTTP route and the MCP `slot.claim` tool both call — a static build check fails if a second implementation or a stray OIDC import appears. We use the Human Continuity IdP over OIDC (authorization code + S256 PKCE, `max_age=0`, freshness judged on `auth_time`, never `iat`), plus the RFC 8628 device grant for the headless agent, with its poll loop detached into an `auth_request` table so the agent can restart and still collect its result. The IdP is a plain OIDC provider and returns **no action-scoped nullifier**, so we reconstruct the IDKit guarantee ourselves: `sha256("humangate/v1/nullifier" | iss | sub | action | signal)`, with `action` bound to `buy_slot:<eventId>` and `signal` to the exact parameters. One-time use is a real SQLite `PRIMARY KEY` — no SELECT-then-INSERT, so two simultaneous double-clicks both reach `INSERT` and exactly one wins — inside the same transaction that confirms the slot. `slot.claim` intentionally does *not* require `approval` in its schema: forbidding the call would hide the failure, so the gate refuses it with `approval_required`, and a client-supplied `{ok:true}` is rejected as `untrusted_client_result`. A disclosed local fallback substitutes the IdP, never the gate.