Book a robot arm, execute tasks. Book on chain, drive from your browser, get paid if you did well.


Hands Unchained — a labor market where verified humans teleoperate real robots, and only AI-graded work gets paid.
The problem. Robot foundation models don't have an internet to scrape. They learn from human demonstrations, collected one teleoperation episode at a time. Tesla and Figure pay operators $25–48/hr to do this on-site. We make that market permissionless and remote: anyone plugs in a robot arm (or a zero-hardware MuJoCo sim), any verified human anywhere drives it over the public internet, and every successful attempt becomes one labeled LeRobot episode in the rig owner's dataset.
The platform. A cloud hub holds a registry of "rigs." A rig is a headless agent running on any Mac — a real SO-101 arm over serial, or a MuJoCo sim. Operators browse a lobby, take a single-writer lease on a rig, and drive it from browser keys, their own leader arm (leader-over-wire), or a phone or VR/AR headset. The rig owner defines a task ("push the cube into the corner"); operators attempt it; the task title becomes the per-frame label; each ✓ writes a real LeRobot episode with operator provenance.
The hole, and the market layer. That loop has one honest flaw: success is self-declared. The operator clicks ✓ and the episode counts. The moment strangers get paid, that hole and two others must close — one per sponsor:
minimum_age: 18) before the lease is granted. Unverified → HTTP 402, the arm stays locked (an unverified visitor can still watch every camera feed — it's an eligibility gate, not a login). Data minimization is strict: one attribute requested, one value kept — the per-action nullifier, which also binds the operator's payout wallet. No name, nationality, or document number ever reaches the hub.{chatID, providerAddress, model, teeVerified, requestHash, responseHash}.SlotMarket gives an operator 30 exclusive minutes on a rig, purchased by staking from their own MetaMask wallet, with an on-chain FIFO queue so nobody can take the arm out from under you mid-episode. Pass → stake returned plus a bonus from the reward pool. Claim success and fail the TEE referee → a strike; three strikes slashes the stake. Nobody profits from a slash — voided stakes return to the reward pool, never a treasury, because the settler is the one component you're asked to trust and it must not also earn from voiding people. settle() is permissionless; a 1 Hz settlement worker executes every payment autonomously on the verdict. The hub never holds or derives an operator key.The cross-chain part is cryptographic, not a promise. 0G's broker signs each inference response inside the enclave with plain EIP-191. contracts/TeeAttestation.sol, deployed on Hedera, rebuilds that digest and ecrecovers it against the signer address 0G's own registry publishes on Galileo (read live at deploy time, not hardcoded). Our backend carries the bytes across and cannot forge them — feed the contract a tampered response and it reverts. Stated plainly, because it matters: this proves the grading output came out of 0G's TEE. It does not prove what was asked, and our backend still chooses which graded attempt to submit. A grade with no attestation (0G down, local heuristic fallback) can still pay you but can never cost you a strike — without the enclave the market may be lenient, never punitive.
Both chains run the same SlotMarket — 0G Galileo (16602) and Hedera testnet (296) — and the operator picks which one to stake on at booking time.
Live testnet contracts: SlotMarket on 0G Galileo 0x0EeFee711Ed37dE9C0d892638F9d14ac9047a9D8 · SlotMarket on Hedera 0x7f37B89DE964DFf7EE6EF6d2E3d58dAF42Ac6863 · EpisodeRegistry 0x80669DE19A96F0004adbC3C81c528Ef1abB9a494 · TeeAttestation (Hedera) 0xE6ad861D1c18d2FeFe18b49bCa1B407587673CC3.
Shape. A Bun monorepo. One TypeScript app (TanStack Start + React 19, Effect v4, Tailwind v4, Vite, Biome) with two entrypoints: src/server.ts is the hub (the deployed web app), src/agent.ts is a headless rig. Alongside it a Python driver (uv, lerobot==0.6.0 pinned, MuJoCo, OpenCV) behind one ndjson-RPC protocol, with two interchangeable backends — real SO-101 over Feetech serial, or a MuJoCo sim. The sim model and IK URDF are vendored, so the repo is self-contained and a rig costs zero hardware.
Transport, and why it's boring on purpose. The rig dials OUT and never listens: a 50 ms HTTP poll carries telemetry up and control down in the same request, and camera frames push separately at 12.5 fps as MJPEG. No WebRTC, no LiveKit, no SDK. That choice buys three things a socket wouldn't: it behaves identically in vite dev and in production, it survives a Railway redeploy with no reconnect logic at all, and every hop is curl-debuggable at a hackathon table on venue wifi. A WebSocket input plane sits on top purely for low-latency teleop, with the HTTP mailbox always live as fallback. Authority is a verb table with per-verb argument allowlists, owner/holder tiers and a 15 s command TTL, plus a single-writer lease — so "who may move this arm right now" is one enum, not scattered ifs.
Market layer, surgically attached. All of it is new files plus one interceptor branch in server.ts, gated on MARKET_MODE. Unset, the platform is byte-for-byte the free version — which was also the fallback demo.
World ID (@worldcoin/[email protected]): the IDKitRequestWidget with the Identity Check preset minimum_age: 18, allow_legacy_proofs: false. rp_context is signed server-side per request (signRequest from @worldcoin/idkit-core/signing, 300 s TTL) and handed to the widget, so the relying-party context can't be swapped by the browser; the result is verified against POST /api/v4/verify/{rp_id}. We keep exactly one value out of it — the per-action nullifier — and bind it to the operator's wallet address, so there's no secondary identity store to leak. The deny path is a real HTTP 402 on lease acquisition, not a hidden button.
0G Compute (@0gfoundation/[email protected], Direct SDK, qwen/qwen2.5-omni-7b): the grader gets the task string plus the sampled telemetry of the episode and returns {score, pass}. We persist the ZG-Res-Key chatID, provider address, model, teeVerified from processResponse(), and sha256 of both request and response — that artifact is the proof-of-inference evidence. GRADER=zg|local switches to a heuristic grader, and the fallback is always wired, because a testnet outage should degrade grading, not break the loop. Episode artifacts go to 0G Storage; its Go-style [value, err] tuples (not exceptions) caught us once.
Evidence capture is the subtle part. market/sampler.ts samples the hub's own rig state at 5 Hz while a ledger row is open — the operator's claim is never an input to grading. Two realities of the stack forced the design, and both were found the hard way: (1) rig.joints freezes while the LeRobot recorder owns the robot bus — and every attempt is a recording — so motion evidence comes from the input plane (browser axes / leader joint targets), which the hub relays and which cannot freeze; (2) the episode save lands 1–2 s after attempt_finish, so sampling continues through a grace window and the worker delays grading to match. A "success" claim with no saved episode auto-rejects before a model is even asked.
Contracts are Foundry (solc 0.8.x), SlotMarket.sol deployed byte-identical to both 0G Galileo (16602) and Hedera testnet (296). Booking is staking: bookSlot(rigId, pinHash) takes payment and a hash of a PIN only the operator knows, so the 30-minute clock starts at startSlot — when they actually show up — not at booking. Per-rig FIFO queue, strikes, endEarly, skipHead, cancel, and a permissionless settle().
The cross-chain link is the hacky bit worth reading. There is no bridge and no light client between 0G and Hedera, and we didn't build a trusted relay. What crosses is the TEE's own signature: 0G's broker signs each inference response in-enclave with plain EIP-191, and TeeAttestation.sol on Hedera re-derives the digest and ecrecovers it against the signer address — which the deploy script reads live from 0G's on-chain registry rather than hardcoding. Our backend carries bytes it cannot forge; a tampered response reverts. recordEpisode on SlotMarket does the same check, which is why the settler key can choose a verdict but never fabricate one — and can't profit from voiding anyone either, since slashed stakes go to the reward pool, not the treasury. Withholding is the only power it has left: a liveness assumption, not a custody one, because settle/cancel/skipHead/withdraw all work without it.
Hedera is raw @hashgraph/sdk — Crypto Transfer plus an HCS topic for provenance — with mirror-node REST verification of every stake. Four traps, each a real bug first: a stake tx hash must fund exactly one bond or a single payment replays into unlimited attempts; for ETHEREUMTRANSACTION the transaction_id belongs to the JSON-RPC relay, so the real sender has to be read from .from on /api/v1/contracts/results/{hash} and long-zero addresses normalized back to the operator's alias; the mirror node reports tinybar while MetaMask speaks wei (a silent 10¹⁰ error); and the escrow account must already exist at its EVM alias, or the first staker pays ~661k gas for hollow-account creation instead of ~22.8k.
Settlement is market/worker.ts: a 1 Hz idempotent state machine walking every ledger row closed → graded → paid/rejected → anchored, every chain call behind a configured guard and a try/catch. It is also the "agent" of the payments story — it decides and submits each transfer itself, on the referee's verdict, with no human clicking pay, and it fires on the verdict rather than a timer so the whole cycle fits in one demo take. viem in the browser (chain defs, MetaMask), ethers on the server (contract calls, EIP-191).
Deploy is Railway config-as-code: Dockerfile from the repo root, 1 replica and sleep off — both load-bearing, since the rig registry is in-memory by design and rigs re-register in ~50 ms after a redeploy.

