GigPay
Instant earned-wage payouts for gig workers, on a double-entry ledger
GigPay records every trip from every platform a delivery or ride partner works for, shows exactly how each trip was paid, and sends money they have already earned to any UPI ID in seconds. Underneath is a double-entry ledger that survives failing banks, lost callbacks and retries, plus an AI money coach that reads the worker's own trips.
- Trial balance always 0
- 40% bank failures survived
- ~3.5 s tap to paid
- Never paid twice
§1
The problem
Gig workers live day to day but are paid week to week, so a Wednesday bike repair means a moneylender. Pay is opaque. And moving money is hard: payout rails time out, banks go down, callbacks get lost or arrive twice. A wallet that pays twice or loses ₹500 is worse than none.
§2
My role
Personal project, end to end: signed trip webhooks and pay rules, the ledger and payout state machine, the mock bank with fault injection, reconciliation, the coach (RAG plus tool calling), the worker app and finance console, and Kubernetes manifests.
§3
Architecture
§4
Key decisions
D/01
Double-entry ledger in integer paise
Every movement is an append-only entry whose legs sum to zero, with a unique idempotency key. Mistakes are fixed with reversing entries, never edits, so the journal is a full audit trail.
D/02
Lock accounts in id order
SELECT … FOR UPDATE in a fixed order means two taps on Withdraw can't both spend the same balance, and concurrent entries can't deadlock. Covered by a concurrency test.
D/03
Payouts as a state machine
CREATED → SUBMITTED → SUCCEEDED / RETRY / FAILED, with exponential backoff, a poller for lost callbacks, and a reversing entry that returns the amount and the fee on permanent failure.
D/04
Earned wage access, not credit
At most 90% of what is already earned, a flat ₹5 fee, up to ₹5,000 and 3 payouts a day. The rules are pure functions with unit tests.
D/05
Reconciliation against the bank
Every minute: each bank transfer must match one succeeded payout, in-transit must equal payouts in flight, and the trial balance must be exactly zero.
D/06
A coach that can only see you
Policy questions get cited hybrid-RAG answers; pay questions call tools over the worker's own trips. The worker id comes from the gateway, never from the model.
§5
Challenges
- The chaos run: 40 payouts with 40% bank failures, 40% lost callbacks, 40% duplicate callbacks and 30% rate limiting. All 40 settled in about 37 s, 36 paid and 4 reversed, with the trial balance at 0 and no reconciliation issues.
- The first version reused the payout id as the bank reference on every attempt, so a retry after a bank failure could never succeed. Now a timed-out attempt reuses its reference and only a definite failure gets a new one.
- Stacking a global rate limiter on per-route limiters made both share one bucket with conflicting settings. Each route now has exactly one limiter.
- HMAC-SHA256 signed trip webhooks with a 5-minute replay window reject unsigned, tampered or replayed reports before parsing.
§6
Stack
- Java 21
- Spring Boot 4
- Spring Cloud Gateway
- Kafka
- Redis
- PostgreSQL
- pgvector
- LangChain4j
- React
- Kubernetes
Next case study
SwiftKart
The engine behind 10-minute grocery delivery