Architecture
This page is for developers who want to understand how Plumb is built under the hood. If you're just trying to use Plumb, Quickstart is a better starting point.
In one paragraph
Plumb is made up of a handful of small, focused programs that talk to each other: a web API (the "gateway") that answers your AI requests, a background worker that handles timed jobs like blockchain settlement, a few small websites (console, explorer, marketing), and a set of smart contracts living on the Base blockchain. Everything runs on a single Linux server — no Docker, no Kubernetes, no cloud platform — and the code for all of it lives in one repository so you can read any part of the system without hunting across services.
The nine workspaces
Plumb is a pnpm monorepo. Every app or package is a workspace, each with its own package.json, tests, and typecheck target. There is no global build — pnpm -r build runs in topological order and nothing is hidden.
apps/
gateway Hono API on port 3000/3010 -- the main HTTP surface
worker Long-lived Node process -- watchers + batcher + pipe fulfiller + memsync
console Next.js 15 operator UI on port 3002/3012
explorer Next.js 15 public read-only UI on port 3003/3013, reads a dedicated pg role
web Next.js 15 marketing site on port 3001, deployed to Vercel
sdk-python Python package (httpx wrapper + ed25519 verify + canonical + merkle)
packages/
auth SIWE nonce issuance + consume, session token create/verify
billing Atomic debit + credit + ledger entries, cost calculation
contracts Foundry project -- Solidity + forge tests + deploy scripts + ABI exports
db Drizzle schema + migrations (0000..0005)
hub-storage StorageBackend interface (fs + optional s3), ONNX session cache
memsync Memory extraction prompt + pgvector writer + profile composer
onchain Event watcher + Merkle batcher + settlement wallet + reorg check + tx queue
pipe JobWatcher (pipe oracle events) + fulfill pipeline
pricing Per-model rate catalog + cost calculator
providers OpenAI, Anthropic, Google, xAI, mock adapters
ui Caliper design tokens + React primitives (Plumb, TickLabel, etc.)
verification ed25519 signer + key registry + canonical payload + merkle helpers
Request flow — chat completion
┌───────────┐ POST /v1/chat/completions ┌────────────────────┐
│ caller │ ────────────────────────────────────▶│ apps/gateway │
│ (SDK) │ │ │
└───────────┘ │ session middleware│
│ rate limit │
│ providers router │
└──────────┬─────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
upstream provider BEGIN TX Redis
(OpenAI / Anthropic) ────────── (BullMQ)
│ receipts.insert │
▼ ledger.debit ▼
response bytes ────────── memsync job
│ COMMIT (if opted in)
▼ ─────────
canonical hash X-Plumb-Receipt
│ back to caller
▼
ed25519.sign
│
▼
JSON response + header
Request flow — receipt → settlement
┌──────────────┐ every 2 min OR ┌───────────────────┐
│ apps/worker │ ─ unsettled > 1000 ────────────▶│ checkpoint batcher │
│ │ │ │
└──────────────┘ │ SELECT unsettled │
│ build Merkle │
│ sign root + meta │
└─────────┬─────────┘
│ runExclusive(signer)
▼
Settlement.checkpointReceipts
│
▼
Base Sepolia
│
▼
UPDATE receipts SET
settlement_tx_hash = ...
Request flow — pipe oracle
┌──────────────┐ 1. oracle.request(modelHash, inputHash, callback)
│ consumer │ ──────────────────────────────▶ PipeOracle on Base
│ contract │ │
└──────────────┘ │ emits JobRequested
▼
┌────────────────────┐
│ pipe-fulfiller │
│ loop (worker) │
└─────────┬──────────┘
│ 2. fetch inputBytes via hash
│ 3. onnxruntime.run (30s timeout)
│ 4. sign EIP-191 over result
│ 5. runExclusive(signer)
▼
PipeOracle.fulfill(id, result, sig)
│
│ verify signer, mark fulfilled
│ try { consumer.pipeCallback{gas: 100_000} }
▼
consumer state updated
Data model
- receipts — every signed completion. Indexed by
(addr, created_at)+settled_onchain_at. - ledger_entries — debits, credits, refunds. Per-addr balance is derived via
SUM(credit) - SUM(debit) - SUM(refund). - hub_models —
(hash, uploader_addr, framework, size_bytes, storage_url, registered_onchain_at). Unique onhash. - pipe_jobs —
(id, requester_addr, oracle_call_tx, model_hash, input_hash, status, result_hash, receipt_id). - memories —
(id, addr, kind, content, embedding vector(1536), confidence, created_at). HNSW index onembedding. - onchain_cursors — one row per watcher. Tracks
last_block_scanned+last_block_hash(reorg detection). - sessions — SIWE-issued tokens, sha256-hashed at rest. TTL 30 days.
Deployment surface
Prod runs on a single Hostinger VPS:
- Caddy terminates TLS for
api.plumbtech.xyz,console.plumbtech.xyz,explorer.plumbtech.xyz. Security headers (HSTS, X-Frame-Options DENY, CSP) are set at the reverse proxy. - PM2 runs four Node services:
plumb-gateway,plumb-worker,plumb-console,plumb-explorer. Restart on crash, persist viapm2 save. - Postgres 16 + pgvector 0.6, one database (
plumb), two roles (plumbfor app,plumb_explorer_rofor read-only explorer). - Redis 7 for BullMQ queues (
plumb-checkpoints,plumb-memsync) and rate-limit sliding windows. - Marketing site (
apps/web) auto-deploys to Vercel on push tomain. Everything else isgit pull && pm2 restart.
Contracts (Base Sepolia)
All verified on Blockscout. See /docs/contracts for live addresses.
- PLMB — ERC-20 credit token with claim faucet (dev) and OZ
ERC20Permit. - Settlement — receives PLMB deposits, checkpoints receipt Merkle roots, enforces signer rotation via Ownable.
- HubRegistry —
register(hash, uploader, metadataURI, prevVersion). Owner-gated (Ownable). Append-only version chain. - PipeOracle — Ownable2Step.
request/fulfillwith signer cosigning + 100k callback stipend +CallbackFailedevent on consumer revert. - PipeConsumer — example IPipeCallback implementer for e2e tests.