BookerBob

Who's behind an agent changes terms, not price. Bots prepay; humans get deposit & pay later.

BookerBob

Created At

ETHGlobal Lisbon 2026

Winner of

The Graph

The Graph - Best AI Use Case of The Graph 2nd place

World

World - AgentKit New Use Cases 2nd place

Project Description

AI agents are about to book, buy, and negotiate for us. When one walks up to a business, the business has two bad options: treat it as fraud and make it prepay everything, or trust it and let bots farm the place. The missing piece is who is behind the request.

BookerBob is a hotel-booking gateway where the answer changes the risk terms, never the sticker price. Same rooms, same nightly rate; what changes is collateral, inventory access, and whether settlement can wait until checkout.

Anonymous bot lane: a short list, 100% prepay, and a real x402 pay-per-query on Hedera testnet (0.01 HBAR per search, a live HashScan tx).

Accountable lane: World proves a real human stands behind the agent (AgentKit verified against AgentBook on World Chain; a browser World ID / Selfie path for people proving themselves). The Graph reads that human's consented onchain history as coarse bands (Messari lending, DEX and perps), never raw numbers, and feeds an underwriting engine. A stronger history earns fuller inventory, a deposit instead of prepay, and rate-lock-now / pay-later as a real Hedera Scheduled Transaction, created at prebook and open on HashScan. Privy binds "this is my wallet" so address consent is proven, not just typed.

North star: package identity plus consented onchain context so an agent books under underwritten terms instead of guessing. BookerBob is the vertical cut: underwriting at the desk, the agent executes.

Live demo (same prompt, two lanes, the term delta live): https://lisbonhack.world/

How it's Made

Notable

Feedback: World App reports success while the browser relying-party gets failure (Selfie / non-orb)

Witnessed by the World team at the ETHGlobal Lisbon booth, who asked us to submit this.

What we observed

  • Two World team members WITH an Orb credential: the flow passed end to end, on both the World App and the browser.
  • Me, WITHOUT an Orb (relying on Selfie / Face): the World App showed SUCCESS, but the browser relying-party surface reported failure ("Something went wrong, we couldn't complete your request"). Reproduced 4 times in a row; the face check never carried through to the web.
  • The World team was surprised: for them even the browser accepted everything; for a non-orb user the app said success and the web said failure for the same integration.

Our configuration (correct, ruled out on our side)

  • /world-id/context returns: app_id app_6e50c804c2328d54285884098331a7a7, rp_id rp_9d8dab44feb6f9e2, action "bookerbob-terms", credentials ["selfie","proof_of_human"], environment "production", with a valid signed rpContext (nonce + signature).
  • allow_legacy_proofs is false (we confirmed true routes to the protocol-3.0 bridge where Selfie Check does not exist).

The evidence that localizes the bug to World's stack

  • Our gateway /world-id/verify endpoint received NOTHING on any of the failed attempts (server logs empty for the whole verify path during the failures). The proof never reached the relying party.
  • So the failure is entirely in the World App -> IDKit/browser bridge leg, BEFORE the proof is delivered to us. The World App told the user it succeeded while the browser side of the same request failed and never called back to the RP. That is an internal inconsistency in World's own flow, not a relying-party error.

Root cause we found (also feedback)

  • In the production World App "Add credential" screen, Face credential (i.e. Selfie Check, credential 11) is shown as "Coming soon" and is not available. A non-orb user therefore has no credential that satisfies our ["selfie","proof_of_human"] request, yet the app surfaces success rather than a clear "credential unavailable" the browser could show the user.
  • The docs describe Selfie Check as a usable credential and promise enrollment-on-demand when a credential is missing; in practice production returns app-success / web-failure with no actionable reason.
  • We could only self-diagnose via POST /api/v1/precheck/{app_id} (is_staging, enable_face_check, can_user_verify), which is not documented in the 4.0 docs; we found it by trial.

Ask

Make the app-side and browser-side outcomes agree, and surface a clear reason (e.g. credential_unavailable) to the relying party when Selfie/Face is not yet live, so an integrator does not chase a phantom failure that only reproduces for non-orb users.

How it's made Monorepo (apps/gateway, apps/web, packages/context-bands-mcp, packages/hedera-schedule). Gateway is Hono on Node 20 / Fly; web is Vite + React 18, same-origin in prod so fetch('/offers') hits the API without a separate CORS host. TypeScript everywhere; no Postgres — process-local Maps for spent ledger, settlement index, World ID rate limits, context cache.

Request path. GET /offers is the desk. Middleware order matters: credential resolve → optional x402 wall → handler.

Header agentkit: @worldcoin/agentkit validateAgentkitMessage + createAgentBookVerifier against World Chain RPC (eip155:480). Fail closed to missing; never return forge-helpful detail to the client. Header world-id: HMAC session from /world-id/verify (IDKit proof → Developer Portal); combines with AgentKit via combine(). Query ?credential=1 with no header → explicit stand_in (browser demo only; cannot become verified by construction). Query ?address= (+ optional Authorization: Bearer Privy access token): jose JWKS verify → linked wallets; only then we treat the address as owned. Address goes to context-bands-mcp over long-lived MCP stdio (HTTP /mcp also exists for curl). Messari subgraphs via The Graph Studio key → bands {activity,tenure,breadth,scale} + repayment signal; thresholds in SKILL.md, raw USD never leaves the MCP. Pure terms.ts: signals in → {tier, inventory, payment} out. No network. Narration strings are the single fact that decided the row. Inventory: booker MCP (RateHawk behind our flexrep wrapper) or fixtures/lisbon.json. If earnsRateLock: @bookerbob/hedera-schedule → @hashgraph/sdk ScheduleCreate; scheduleUrl = hashscan.io/testnet/schedule/<id>. Bot metering. Uncredentialed /offers registers x402 (@x402/hono + @x402/hedera, network hedera:testnet, facilitator Blocky402, price 0.01 HBAR). Race UI doesn’t do client-side payment UX — it POST /x402/paid-offers, gateway wraps fetch with wrapFetchWithPayment + demo LISBON2026_HEDERA_* signer, strips settle tx from PAYMENT-RESPONSE into x-bookerbob-payment-tx / -url. UI increments spent only if that header exists.

Web. Two lanes parallel: bot = metered paid-offers; backed = credentialed /offers. Machine view is a protocol transcript over the same JSON (some rows live, some declared — don’t trust the wire sketch alone). Overview opts out of metering (metered: false) so a human reading the page isn’t charged as an agent.

Partners, concretely. World → who is accountable. Graph → underwriting file without doxxing balances. Hedera → money movement (x402 per query + scheduled checkout). Privy → SIWE + server-side address bind. x402 → 402 challenge/settle without API keys for bots.

Hacky bits worth saying. (1) Demo operator keys for both HBAR payer and (script) AgentKit agent — shop-window parallel to “facilitator pays,” not user wallets. (2) World RPC death used to look like “wallet not registered”; fixed with a raw eth_blockNumber probe in world-chain.ts. (3) AgentKit maxAge is ms not seconds — we shipped a 0.3s window once; fixed after staging. (4) HashScan broke when we used #/testnet/...; current explorer wants path URLs. (5) Schedule execute exists as POST /book / CLI demo; race UI only surfaces ScheduleCreate + HashScan (create is the on-chain proof in-window).

background image mobile

Join the mailing list

Get the latest news and updates