← All work

TokenTrade Capacity Exchange

A two-sided MCP exchange where idle Claude Code sessions clear escrowed coding tasks.

Aug 2026Personal

Next.js 16React 19TypeScriptMCP TypeScript SDK (Streamable HTTP)Postgres on NeonDrizzle ORMnode-postgresZodVitest + embedded-postgresOpenRouterResendTailwind CSS 4VercelGitHub Actions

Problem

Claude Pro and Max capacity resets every cycle whether it was used or not, and the obvious way to sell the idle part, letting someone else drive your session, is account sharing and a security hole. This began as a personal queue for feeding well-specified tasks to one's own idle session over MCP, then became a two-sided exchange: one account's Claude Code submits escrowed tasks, another's pulls and works them in its own environment. The hard parts were settlement neither side can game, running a stranger's task safely, and keeping a networked tool honest about auth.

What I built

A Next.js 16 app hosting stateless Streamable-HTTP MCP endpoints, /api/mcp/buy and /api/mcp/sell, plus the TokenTrade.ai front door and an owner dashboard. A task is an envelope (goal, repo or workspace, done_when, verify_cmd, budget cap) so 'exit 0' is the contract, and submitting escrows a fixed price in an append-only credit ledger. A seller atomically claims the oldest eligible task, receives a claim nonce, delivers a patch plus verify output, and the task parks pending; only the buyer's confirm or decline moves credits, and the patch is withheld until confirm. Every submit is screened by an LLM before any row exists, and screening fails closed. Hand-authored SQL migrations run on Neon Postgres with Drizzle as a typed query layer that never generates schema; buying is live and selling is gated per account until a seller sandbox ships.

Highlights

  • Settlement neither side can game: a seller's self-report never settles anything; the buyer's confirm splits the escrow and decline refunds it whole; the patch is withheld until confirm so work cannot be harvested and refused. Credits enter only by deliberate grant, because a free opening balance is a Sybil faucet once anyone can open an account.
  • Three credentials for three questions: sha256-looked-up account tokens with the role decided per tool call; per-task capability pairs (scrypt, narrowed view) checked against a decoy hash so an unknown id and a wrong secret cost the same time; and revocable share pairs whose secret rides in the URL fragment so it never reaches a log.
  • Fail-closed intake: every submit is judged by a flash-tier model over OpenRouter under a strict JSON schema; deny, needs-review, timeout or unparseable output all mean no row and no escrow, the claim query separately requires an allow verdict, and the dev stub is loud and recorded on every row it passes.
  • Ran it against real Neon and wrote down what broke: an idle-connection error with no listener kills the Node process, pg ignores ?channel_binding=require, an explicit ssl option is silently overwritten by the URL, and a bare SET leaks between clients through the pooler. The URL is parsed once into explicit config; the app uses the pooled host, migrations the direct one.
  • Two CI suites with no database secret anywhere: in-process Vitest against a throwaway embedded Postgres (reads TEST_DATABASE_URL, never DATABASE_URL, and refuses a *.neon.tech host), plus an over-HTTP suite that builds the app, starts it, and drives it with the real MCP SDK client. The server refuses to boot on a missing, placeholder or short owner token.

Numbers

  • 80 concurrent client connections through Neon's pooled endpoint were served by 17 server backends; 80 through the direct host cost 80 backends
  • Cold start measured at 975ms vs a 368ms warm connect, roughly 600ms paid once; suspend threshold observed between 7 and 14 minutes idle
  • 337 test cases across the in-process and over-HTTP suites
  • 45 commits and 28 merged pull requests in five days (2026-08-09 to 2026-08-13)
  • Seller claims age out after one hour; dashboard links are single-use and expire in two minutes; dashboard sessions last 12 hours
  • Intake screen bounded at 8K characters per field and a 10s timeout, with one retry on a network error or 5xx only

Screenshots

TokenTrade Capacity Exchange: TokenTrade.ai landing, live, sell mode: the whole settlement as a seven-line terminal, claim, test, report, pending, confirmed.
TokenTrade.ai landing, live, sell mode: the whole settlement as a seven-line terminal, claim, test, report, pending, confirmed.
TokenTrade Capacity Exchange: Owner dashboard, Architecture view: buyer and seller Claude sessions push and pull through /api/mcp/buy and /api/mcp/sell into one exchange and one ledger. Rendered locally from fixtures.
Owner dashboard, Architecture view: buyer and seller Claude sessions push and pull through /api/mcp/buy and /api/mcp/sell into one exchange and one ledger. Rendered locally from fixtures.
TokenTrade Capacity Exchange: Owner dashboard, Queue view: status and outcome as two axes, queued, running, pending, verified, failed. Fixture data, not production state.
Owner dashboard, Queue view: status and outcome as two axes, queued, running, pending, verified, failed. Fixture data, not production state.