Three small money-movement services: payout creation and lookup, applicant verification, and a
shared ledger. TypeScript on Node 20+, npm workspaces, Express 5, Vitest, and tsx — no build
step. Each service owns its src/, tests/, and package.json dependencies; nothing is shared
between services on purpose.
| Folder | Purpose | Dev port | Routes |
|---|---|---|---|
payments/ |
Payout creation and lookup | 4001 | GET /health, POST /payouts, GET /payouts/summary, GET /payouts/:id, GET /accounts/:accountId/payouts |
kyc/ |
Applicant verification rules | 4002 | GET /health, POST /applicants/:id/verify, GET /applicants/:id/status |
ledger/ |
Accounts, balances, transfers | 4003 | GET /health, POST /accounts, GET /accounts/:id/balance, GET /accounts/:id/entries, POST /transfer |
Auth is a bearer token: payments and kyc check it per-route via verifyToken; ledger
checks it through its requireAuth middleware.
Three independent Express apps, one per folder, each with its own in-memory store, its own
copy of the token check, and its own tests. No service calls another — a caller talks to each
one directly. The only outbound call is payments → the payout provider, and only when
PROVIDER_LIVE is on.
flowchart LR
client([Caller<br/>Bearer tok_<principal>])
subgraph payments ["payments/ :4001"]
direction TB
pAuth["auth.ts<br/>verifyToken + requireScope<br/>scopes: payouts:read, payouts:write"]
pRoutes["app.ts<br/>POST /payouts<br/>GET /payouts/summary<br/>GET /payouts/:id<br/>GET /accounts/:id/payouts"]
pStore[("payouts.ts<br/>Map<id, Payout>")]
pProv["provider.ts<br/>submitToProvider"]
pRoutes --> pAuth
pRoutes --> pStore
pRoutes -. PROVIDER_LIVE .-> pProv
end
subgraph kyc ["kyc/ :4002"]
direction TB
kAuth["auth.ts<br/>verifyToken + requireScope<br/>scopes: kyc:read, kyc:write"]
kRoutes["app.ts<br/>POST /applicants/:id/verify<br/>GET /applicants/:id/status"]
kRules["verification.ts<br/>parseApplicant → runChecks → evaluate"]
kStore[("results<br/>Map<applicantId, CheckResult>")]
kRoutes --> kAuth
kRoutes --> kRules
kRoutes --> kStore
end
subgraph ledger ["ledger/ :4003"]
direction TB
lAuth["auth.ts<br/>requireAuth(scope) middleware<br/>scopes: ledger:read, ledger:write"]
lRoutes["app.ts<br/>POST /accounts<br/>GET /accounts/:id/balance<br/>GET /accounts/:id/entries<br/>POST /transfer"]
lStore[("ledger.ts<br/>accounts · entries · byIdempotencyKey")]
lRoutes --> lAuth
lRoutes --> lStore
end
provider[/"External payout provider"/]
client --> pRoutes
client --> kRoutes
client --> lRoutes
pProv --> provider
Every service follows the same file layout, so one agent can own one folder without reading the others:
flowchart LR
server["server.ts<br/>listen on the dev port"] --> app["app.ts<br/>createApp(): routes + error handler"]
app --> auth["auth.ts<br/>token → Principal { sub, scopes }"]
app --> domain["domain module<br/>payouts.ts / verification.ts / ledger.ts<br/>validation + in-memory store"]
tests["tests/*.test.ts<br/>createApp().listen(0) + fetch"] --> app
Principals are fixed per service (tok_ops, tok_analyst · tok_onboarding, tok_support ·
tok_treasury, tok_reporting); amounts are integers in cents; a transfer carries an
idempotencyKey so a retry cannot move money twice. The conventions behind those choices live in
.claude/skills/money-movement-conventions/SKILL.md.
Requires Node 20+.
npm install # once, at the root
npm test # all three services
npm test -w kyc # one service (also: npm run test:kyc)
npm run dev:payments # start one service
npm run test:stable # excludes one test that's flaky by design
npm run check # repo self-checkpayments/,kyc/,ledger/— the three services, each with its ownsrc/,tests/, and dependencies.claude/— skills, agents, and hooks Claude Code uses in this repoevals/— an eval kit for one of the skillsissues/,grading/— three issue files (one per service) and the hidden tests that grade a fixtools/— a token-usage ledger, a status line, a scorecard, and a repo self-checkbuild-it/,solutions/— a bonus build track and its finished reference
This repo is also the reference repo for the Advanced Claude Code workshop. A few things are wrong on purpose — one test is flaky by design — so an attendee can practice finding and fixing real defects with Claude Code. Read CASE.md for the story and WORKSHOP.md for the full workshop guide.
CLAUDE.md holds the rules Claude Code reads at every session start.