Overview
What it is
Settleo is a non-custodial peer-to-peer and OTC crypto-settlement platform, built as a ~24-repo polyrepo — about eighteen hexagonal (ports-and-adapters) TypeScript backend services, plus the consumer/business/console apps, the Solidity escrow contracts and a shared library. Retail users trade through a consumer app, desks through a business app, and operators watch it all from an internal console — but every surface is downstream of one rule: value moves in exactly one place.
The problem
When money is on the line, "a bunch of services that each touch balances" is a recipe for double-spends and irreconcilable state. The hard part isn't building one service — it's building eighteen of them without letting any two of them disagree about how much money exists. Settleo's answer is architectural: a single value authority, a deny-by-default trust model on every hop, integer-only money, and boundaries that are stubbed honestly rather than faked when the real integration isn't built yet.
The service topology
Traffic enters through settleo-gateway (:8080), a stateless edge/BFF with no
database and no domain logic — its only store is Redis (rate-limit, nonce,
idempotency). It recognises three credential shapes and denies everything else
with a uniform 401; its route table is the allow-list, so an unmatched route
404s before any auth runs. The console, by contrast, is a trusted internal client
that signs service-to-service directly to the domain services rather than
passing through the gateway.
| Tier | Service | Responsibility | Writes balances? |
|---|---|---|---|
| Edge | settleo-gateway | AuthN of 3 credential shapes, route allow-list, BFF request adaptation, recipient-gated WebSocket fan-out | No |
| Identity | settleo-iam (:8081) | Login/sessions, alg-pinned HS256 tokens, refresh-rotation with reuse detection, permissions-list authz | No |
| Value authority | settleo-ledger | Sole writer to TigerBeetle; double-entry transfers + two-phase holds over gRPC | Yes — only here |
| Money movement | wallet, escrow-orchestrator, trade-engine, payments | Drive deposits, escrow, trade sagas, fiat legs — all through the ledger client | No |
| Risk & control | risk, compliance, review, dispute, reconciliation | Withdrawal holds, KYC gates, operator review cases, 2-of-3 arbitration, drift checks | No |
| Market & social | pricing, offer, reputation, messaging, notifications | Price ticks, order book, ratings, chat, alerts | No |
| Chain & devs | indexer, developer | Reorg-safe on-chain ingestion, developer API keys | No |
Every service enforces its own deny-by-default S2S authorization rather than trusting the gateway. IAM's authorizer is a permissions-list model — not RBAC — where explicit Deny always beats Allow (the AWS rule).
How money actually moves — the trade settlement path
A cooperative trade settles as a saga, and the interesting property is that no single step is trusted to be atomic across services — every hop is idempotent and legal-only:
- trade-engine drives a trade FSM and asks the escrow-orchestrator to escrow the crypto leg.
- The orchestrator opens + funds the on-chain
SettleoEscrow, then mirrors the lock into the ledger as a two-phase hold (seller → escrow). - On a 2-of-3 release, the orchestrator posts the payout to the ledger as one
all-or-nothing linked batch — commit the hold, pay escrow → buyer (net),
escrow → fee — tagged
settlement{tradeId, transitionId}. - That settlement tag is what makes the ledger emit
TradeSettled(once per batch) plus aLedgerTransferPostedper transfer, through a transactional outbox onto Redis Streams. - trade-engine consumes
TradeSettledand reachescompleted.
The withdrawal path is the mirror image of trust: a wallet withdrawal is
risk-assessed, and a risky one becomes a ledger hold plus a WithdrawalHeld
event that opens a settleo-review case; an operator decides in the console, and
WithdrawalReviewDecided resumes or rejects the wallet. The money is reserved,
never released, until a human signs off.
The crown jewel: a single-writer ledger
settleo-ledger is the only writer to TigerBeetle and the only authority on
balances, exposed as double-entry transfers and two-phase holds over a versioned
gRPC contract (settleo.ledger.v1.Ledger). Every other service reaches money
through its @settleo/ledger-client port — types only, no TigerBeetle access
escapes the ledger. Accounts and transfers map straight onto TigerBeetle
primitives:
| Concept | Field | Meaning |
|---|---|---|
| Account | ledger | one ledger space per asset (e.g. ETH, USDC) |
| Account | code | account type chosen by callers: user / fee / escrow / gateway / world |
| Account | allowDebitsExceedCredits | system/funding accounts (e.g. world) may run a debit balance; customer accounts may not |
| Balance | availableBalance | credit-normal: creditsPosted − debitsPosted − debitsPending |
| Transfer | flag | single | pending | post_pending | void_pending |
| Transfer | amount | u128 integer minor units; decimal string on the wire, bigint internally |
Account codes and ledger spaces are chosen by callers, not fixed in the ledger
repo, so the taxonomy stays open (crypto ETH/USDC spaces; fiat mirror accounts at
a higher offset with allowDebitsExceedCredits because fiat isn't custodied
on-platform). A rebuildable MongoDB projection is derived from the transfer
log and is never the source of truth — deterministic string→u128 id encoding
preserves idempotency across the boundary:
export function encodeId(id: string): bigint {
const digest = createHash('sha256').update(id).digest(); // 32 bytes
let value = 0n;
for (let i = 0; i < 16; i++) value = (value << 8n) | BigInt(digest[i]!);
return value === 0n ? 1n : value; // TigerBeetle rejects a zero id
}
Money model & correctness
Money is integer minor units (u128-safe bigint), never floats; it crosses the
wire as a decimal string bounds-checked against U128_MAX. The ledger's pure
reference reducer encodes six invariants — double-entry always balances, no
overdraft by construction, idempotent replays, pending posts-xor-voids, holds
auto-void on timeout, linked transfers commit all-or-nothing — and doubles as
both the spec the TigerBeetle adapter must match and a deterministic fake for
higher-layer tests. Because TigerBeetle and Mongo cannot share a transaction,
durability for that gap is honestly stated as rebuild-from-log, and a
reconciliation job cross-checks the committed projection against TigerBeetle
field-by-field on an interval. drift = 0 is a HARD non-functional requirement —
but it is meaningful only once the real event sources are wired, which the repo's
per-cell tracker says out loud rather than dressing up.
Internal contract & transport security
gRPC is the canonical internal contract (org ADR-006): .proto packages are
versioned, and a breaking change requires a new package version, never an in-place
edit. Every internal call carries a body-bound HMAC — the caller signs
[METHOD, ts, nonce, sha256(canonicalBody)] where METHOD is the fully-qualified
RPC path (so a signature can't be replayed onto a different RPC), and the verifier
checks the HMAC in constant time, enforces a clock-skew window, and burns the
nonce once via a durable Redis SET NX PX guard that makes replay protection
cross-instance and fail-closed. Signing a gRPC body is a genuinely hard problem
because proto3 elides default-valued fields on the wire; Settleo solves it with a
transport-neutral canonical form (recursively drop proto3 defaults, sort keys,
JSON-encode) computed identically by signer and verifier.
Testing & security posture
Ledger invariants are property-tested with fast-check; integration tests run
against real TigerBeetle + a Mongo replica set via testcontainers. STRIDE threat
models were authored before feature code, targeting OWASP ASVS L3 for
money-tier services — the ledger's model maps eleven threats (L1–L11) each to a
mitigation and a security test (overdraft-by-construction, replay-is-a-no-op,
post-xor-void, all-or-nothing rollback). Boundary adapters that aren't built yet
(MPC signer, PSP, chain-balance reads) are stubbed and fail closed rather than
faked, and the honesty is a documented product rule: domain logic and FSMs are
real and tested; simulated seams are labelled as such.