Search projects, hackathons, sponsors and tech

    ← All projects

    Legwork

    Agents hire World ID-verified humans for real-world checks; USDC escrow releases on proof.

    Demo

    About

    Agents hire verified humans for the legwork software can't do. Escrow releases on proof. An agent posts a real-world task (confirm a shop is open, photograph a poster, read a sign, make a short call to the place) and funds it in USDC escrow on Base Sepolia. A World ID-verified person nearby claims it, does it, and submits proof. The escrow releases on proof. Marketplaces already let agents hire humans. Legwork is the first where every worker is one verified human, every payment is escrowed onchain and released on proof, every hiring agent is accountable, and the documented abuse classes are refused at the API. The trust model. Verification proves a worker is a live, unique person. It does not prove they are honest or competent. Escrow bounds the agent's loss to one task, and a per-agent daily cap bounds it to one day. Screening is a cost floor, not a cure. Legwork guarantees bounded, attributable work: an agent never pays for nothing, a worker never works for nothing, and every task leaves a record both sides can read. Bot-proof, not fraud-proof. The loop. A worker proves personhood with World ID through IDKit and registers one account. The agent asks for one of four typed things and pays through x402: 3.00 to the worker plus a 0.45 fee on top, so 3.45 is charged to the agent. Money is locked before anyone can claim. The escrow records the x402 payer as the buyer and the refund party, and a per-task cap plus a per-agent daily cap (5 open tasks / 25 USDC) bound the loss. The worker signs in with World ID and their World App wallet; we relay the claim and pay the gas, so a worker never needs ETH. Release happens on approve, or by autoRelease after the task's dispute window. Expiry refunds the buyer. A contested proof goes to operator resolve, which charges zero fee on either leg. Both records move: worker reputation is nullifier-keyed and deduplicated per rater, and the hiring agent's ERC-8004 record gets paid-on-proof, disputed or task-refused:<class>. A refused task moves no money. Selfie Check is the credential, at login and again at claim. IDKit 4.x selfieCheckLegacy both times (QR on desktop, deep link on a PWA, World App bridge in the mini-app), against production World App; orbLegacy is the fallback for a World ID with no face credential. The second check, at claim, is spent once and is the live-person signal: a human is behind this phone for this errand, not only at sign-up. World calls Selfie Check medium-assurance and says it does not guarantee strict one-person-one-account uniqueness like Orb verification, so the banner reads "a live person, camera-checked" instead of "one account per person". One nullifier still binds to one worker in the registry; what we no longer claim is that one human cannot hold two. See FEEDBACK-WORLD.md E5–E13 and docs/spikes/RESULTS.md ## S2. Negative attestations. AbuseMark holds the Task API's own registered ERC-8004 identity and is the only writer of agent-side feedback, so every mark has a named author a reader can look up. The six refusal classes are taken verbatim from the abuse literature: credential fraud, identity impersonation, automated reconnaissance, social media manipulation, authentication circumvention, referral fraud (Mehta, arXiv:2602.19514). The agent id a mark names is resolved from the payer through the ERC-8004 IdentityRegistry and never read from the request body, because a subject you did not authenticate is a subject anyone can frame. The four task types and their floors (what the worker keeps): verify-open 3.00 · photo-of 3.00 · call-confirm 2.00 · compare-two 1.00. On a 3.00 task the agent pays 3.45. Out of scope. Competence vetting: a World ID nullifier proves a live, unique person, not that they are honest or skilled, and there is no background check. Dispute arbitration: v0 is agent-approve, auto-release after the task's window, expiry-refund, and operator resolve for a contested proof. KYC and payroll: workers are paid per task in testnet USDC in the demo; tax reporting, employment classification and mainnet payouts are not in this repo. Fraud-proofing: GPS is self-reported and spoofable, a photo is anchored, not authenticated, and the loss is bounded at one task. State of the demo. Testnet USDC on Base Sepolia. One real World ID registration (the demo worker's phone) plus 23 seeded worker rows, flagged onchain as seeded and rendered as such, never presented as people. Zero self-funded external posters during the hackathon, logged in POSTERS.md; the subgraph agrees (PosterStats { distinctExternalBuyers: 0, externalTasks: 0 }). Today the operator is on both sides of the transaction: my agent, my phone, my key resolves disputes. The contract applies the same rules whoever is on each side. Bazantic. Username RubenSousaDinis. Two x402/MPP gateways: the Legwork Task API at nf26bnkznrbc3cg5rbq2hmej5q.bazgateway.com and a new Overpass (OpenStreetMap) service at vz23lkwccfa6hfawpnzw5ohzyy.bazgateway.com, an API that was on no sponsor list. The recipe place-anywhere-then-quote chains them: resolve a place anywhere, screen and quote it through Legwork /check, and stop at the unpaid /tasks 402. It is published in Bazantic and in examples/recipes/. Screen recording: https://www.loom.com/share/1942b481782740e28feeb72f9b4346aa (1:36). Feedback: FEEDBACK-BAZANTIC.md (B1–B3). Addresses (Base Sepolia, chain id 84532, deployed 2026-09-07T11:33:42Z). Ours: WorkerRegistry 0xc33d229046507f4C2E664cbf974542c92eEAbAf4 · TaskEscrow 0x641B56dfA3A033D84a75588c18579347A0DE3c6B · Reputation 0x2f731B56D02080190fa2ef7813887B2743551E43 · AbuseMark 0x29145D47EFc76bEaBc3A4011cFf7fC0fBEa02608. Live, not ours: ERC-8004 IdentityRegistry 0x8004A818BFB912233c491871b3d84c89A494BD9e · ERC-8004 ReputationRegistry 0x8004B663056A597Dffe9eCcC1965A193B7388713 · USDC 0x036CbD53842c5426634e7929541eC2318f3dCF7e. Live surfaces. Dashboard https://legwork-dashboard.vercel.app · mini-app https://legwork-miniapp.vercel.app · Task API + MCP https://legwork-api.vercel.app/mcp · subgraph https://api.studio.thegraph.com/query/74763/legwork-base-sepolia/6653cb4. Try it: claude mcp add --transport http legwork https://legwork-api.vercel.app/mcp, then ask your agent "Ask Legwork whether <place> is open right now". The six tools and four task types are documented in SKILL.md and docs/mcp.md. Limits: verify-open, photo-of and call-confirm need a real business with an OpenStreetMap id; Leiria and Lisbon resolve from a packaged index, anywhere else resolves live with one Overpass request. The worker pool is one real worker plus a hand-recruited standby crew in Leiria. Answers come back in minutes, not milliseconds. Start Fresh disclosure. Pre-kickoff artifacts: a planning pack, a pitch deck and a static UI mockup, all dated and public. No code or stylesheet from them is in this repo; the plan is reproduced verbatim under docs/plan/ so the disclosure is checkable.

    How it's made

    Stack. A pnpm + TypeScript monorepo. Four Foundry contracts on Base Sepolia: WorkerRegistry, TaskEscrow, Reputation, AbuseMark. A subgraph on The Graph Studio indexing all four. Three Next.js apps on Vercel: the Task API (which also mounts the MCP server at /mcp), the worker mini-app that runs inside World App, and the public read-only dashboard. Six packages: chain, payments, screening, mcp, subgraph-client, shared. viem everywhere for chain access; Postgres behind the Task API for nonces, claims and proof metadata. The Day-1 spikes locked the architecture before any product code. S1 (PASS): the World ID Router on Base Sepolia answers, onchain verification is Orb-only, and WorkerRegistry ships one cloud-verified ATTESTED mode. S2 (PASS, after a DOWNGRADED first pass): IDKit 4.x verifies Orb end to end; Selfie Check returned verification_disabled until the Beta flag was granted on 2026-09-08, and it was confirmed working against production World App on 2026-09-13. The product presents Selfie Check twice, at login and again at claim, with orbLegacy as the fallback for a World ID that has no face credential. Selfie Check is medium-assurance, so the product claims "a live person, camera-checked" and not Orb's one-account-per-person. S3 (PASS): exact-EVM x402 against the reference facilitator, settle after post, nonce-keyed idempotency, and the buyer paid no gas; PAYMENT_MODE stays x402. S5 (PASS): the production ERC-8004 IdentityRegistry 0x8004A818… and ReputationRegistry 0x8004B663… are live on Base Sepolia with their interfaces confirmed, so we did not self-deploy a registry. Locked: Selfie Check at login and at claim (Orb as fallback) / GPS available / payment x402 / ERC-8004 live registries. Partner technologies, and what each one bought us. World ID gives the one-nullifier-one-worker constraint the trust model rests on. WorkerRegistry reverts a duplicate nullifier (test_Register_DuplicateNullifierReverts), so the supply side cannot be inflated by bots the way a signup form can; none of our nearest competitors has this. The worker signs in with World ID and their World App wallet; we relay the claim and pay the gas, so a worker never needs ETH, and the contract records their address. Registration is cloud-verified, operator-attested — onchain World ID verification is Orb-only today. Thirteen dated feedback entries (E1–E13) are in FEEDBACK-WORLD.md, including the undocumented verification_disabled response, the credential name that appears only inside IDKitDebugReport, and the 2026-09-13 resolution. The Graph is load-bearing. The preflight_workers MCP tool answers "is anyone who can do this around right now?" from the subgraph before the agent spends anything, and the agent's prompt requires it to quote n_real and median_source back to the user, so a seeded row can never pass as a person and a median computed from seeded data is labelled as such. The hosted mount in apps/api/app/mcp/route.ts injects createSubgraphClient from packages/subgraph-client; examples/transcript.md is a captured session in which the agent reads those fields. x402 lets an agent pay over a plain HTTP 402 round trip with no wallet ceremony and no gas. The escrow records the x402 payer as the buyer and therefore the refund party, which is what makes "the agent never pays for nothing" enforceable. ERC-8004 gives the hiring agent an identity other services can read, which is what makes a refusal worth writing down. AbuseMark holds the Task API's own registered ERC-8004 identity, so every mark has a named author; the subject agent id is resolved from the authenticated payer through the IdentityRegistry and never read from the request body. Bazantic fronts two services as x402/MPP gateways, our Task API and a new Overpass (OpenStreetMap) gateway, and the recipe place-anywhere-then-quote chains a live place lookup into Legwork's /check quote. Since Day 8 the Task API resolves any OpenStreetMap id live with one Overpass request; Leiria and Lisbon come from a packaged extract. Username RubenSousaDinis; the full record is in docs/bazantic.md. Screening is the unglamorous part. There is no free-text field anywhere in the API. The four task types are field-level schemas, so a prompt-injected request has nowhere to land and free text never reaches the classifier. The deterministic keyword gate is authoritative; the LLM (claude-opus-5, 3 s timeout, falling back to the keyword class via CLASSIFIER_TIMEOUT_MS = 3000) may add a refusal but can never overturn one. A fixture corpus in packages/screening/fixtures runs in CI. A malformed request gets a plain 4xx and never produces a task-refused mark; only a well-formed request that hits one of the six classes does. Hacky bits. Base Sepolia reads lag their own receipt, so the API resumes a claim instead of trusting a single read. simulateContract predicts the same task id twice under concurrency, so minted ids are read from the receipt's event. AbuseMark holds an immutable cross-contract reference to its own ERC-8004 id, so a one-contract fix cost a four-contract redeploy. The x402 client's default spend cap is one dollar and throws on a 3.45 charge. AI assistance, documented. Every commit carries an AI-Usage: trailer naming the tool, the model, what it drafted and what a human reviewed or edited; CI fails a pull request with a commit that lacks one. Claude Code (Opus 5 and Fable 5.1) ran the task agents, one per git worktree, plus cloud sessions with no key or .env; Cursor (Grok 4.6, Composer) ran the Day-8 tasks; a lead session in Claude Code wrote the briefs and reviewed every pull request. The prompts are committed: examples/prompt.md for the demo agent and packages/screening/src/classifier/ for the screening prompt. docs/AI-USAGE.md compiles all 474 trailers on main with a row per pull request and per commit; none are rolled into an "other". The Reputation contract was re-implemented from the same threat model, written from a blank file after kickoff.