Draft — Response to Client Questions, 2026-04-30

End-of-Day Service & Account FSM

Operational Lifecycle, Reconciliation, and State Management
11 EOD Responsibilities
8 Account States
3 Recon Boundaries
~11d Engineering Effort
§ 1 · Client Questions

The Three Questions

Verbatim from Richard-HFT, 2026-04-30 07:32 GMT+9

01

Did we include EOD in the full account lifecycle?

Creation, suspension, reactivation, balance adjustments, and cross-system reconciliation.

Not yet
02

Will we need a finite state machine to manage account states?

Today we have a 3-value enum with no transition guards, audit trail, or side-effect hooks.

Recommended
03

Are there any EOD specs?

Across all received documentation: tokenomics, staking, system spec PDF, delivery plan.

None received

Bottom line

The platform's substrate for EOD already exists — balance-snapshot tables, ledger schema, status enums, NATS event bus. But there is no scheduled service running the close, no formal FSM enforcing valid transitions, and no written specification we can implement against. This proposal scopes all three.

§ 2 · Current State

What's Already in Place

Substrate audit — verified live on production EC2, 2026-04-30

ComponentTypeStatusLocation
ledger_entries tablePostgres schemaSchema presentquantatrade.public
user_balance_snapshots tablePostgres + Merkle leavesSchema presentFK → proof_of_reserves
accounts tablePer-user × per-currencySeeded3 rows live
UserRegistry (matching engine)In-memory balanceLiveexchange-core2
users.status enumactive / suspended / banned3 values, no FSMUserStatus enum
NATS event busorder.* / trade.executedLivews-gateway forwards 6 types
Scheduled job runnercron / @Cron / BullMQNoneZero across 10 services
Account state transitions audit tablePostgresNot presentRequired for compliance
Cross-system reconciliation jobME ↔ DB ↔ custodyNot presentMentioned in code comment only
§ 3 · EOD Service Scope

Eleven Responsibilities

Standard centralised-exchange EOD coverage, mapped to milestones

01

Balance snapshot

Read matching-engine UserRegistry total + locked + available per currency, hash, write to user_balance_snapshots.

M1 amendment
02

P&L calculation

Realised P&L from the day's trades; unrealised from open positions × mark price.

M1 amendment
03

Fee accrual

Sum maker/taker fees per user, write a journal entry to ledger_entries with audit trail.

M1 amendment
04

Volume rollup

Update 30-day volume tracker that drives fee tiers (already partial in fee/VolumeTracker.java).

M1 amendment
05

Staking reward distribution

Pro-rata $QTRA payout per active staker; revenue-router gated.

M5
06

ME ↔ DB reconciliation

For every (user, currency): assert UserRegistry.total == Σ(ledger_entries.amount). Mismatch pages + freezes.

M1 amendment
07

DB ↔ custody reconciliation

For every asset: Σ(users.balance) ≈ BitGo wallet balance ± hot-wallet float tolerance.

M5
08

DB ↔ on-chain reconciliation

staking_positions.amount totals reconcile to StakingPool.totalStaked() on-chain.

M5
09

Auto-suspend on sanctions hit

Compliance hook fires FSM transition to suspended + cancels open orders.

M4
10

Reactivation on KYC re-verify

Clears suspended status, restores trading + withdrawal privileges with audit entry.

M4
11

Admin balance adjustments

Manual credit/debit lands as ledger_entries with reference_type='admin_adjustment' + reason.

M4
§ 4 · Architecture

EOD Service Architecture

Idempotent, leader-elected, observable

Single new service

platform/services/eod-service/ — Node.js, same template as ledger-service.

🔒

Leader election

Postgres advisory lock or Redis SET-NX so multi-instance deploys don't double-run the close.

Idempotent steps

Each step keyed on (YYYY-MM-DD, step_name) in eod_runs — re-running a failed step doesn't double-account.

📡

NATS event surface

eod.snapshot.completed, eod.recon.failed, etc. Admin panel subscribes.

🔧

Manual trigger

gRPC AdminService.RunEOD(date) for back-fills and disaster recovery.

Hard timeout

30 min per step. Anything longer pages on-call automatically.

// Pseudocode for one EOD pass — idempotent, ordered, observable async function runEOD(date) { await acquireLeaderLock(); for (const step of [snapshot, pnl, fees, volume, recon_me_db]) { if (await alreadyRan(date, step.name)) continue; try { await step.run(date); await markRan(date, step.name); publish(`eod.${step.name}.completed`, { date }); } catch (e) { publish(`eod.${step.name}.failed`, { date, err: e.message }); throw e; // downstream paged via subscription } } }
§ 5 · Account FSM

Eight-State Account Lifecycle

Today: 3-value enum, no guards, no audit. Proposed: typed transitions, immutable history, side-effect hooks.

pending_kyc kyc_rejected kyc_approved active suspended restricted closed banned compliance hold withdrawal-only terminal
StateMeaningTradeWithdraw
pending_kycaccount created, KYC submitted, pending review
kyc_rejectedKYC failed, can resubmit
kyc_approvedKYC passed, awaiting first deposit
activenormal operation
suspendedcompliance hold (re-KYC, sanctions match, dispute)
restrictedwithdrawal-only (off-boarding, abandoned account)
closeduser-initiated termination, balances zeroed
bannedterminal — fraud, sanctions hit, court order
// Typed transition API — invalid transitions reject at compile + runtime type Transition = { from: AccountState; to: AccountState; reasonCode: string; authority: 'self' | 'admin' | 'system'; }; function transition( userId: string, target: AccountState, ctx: TransitionContext ): Result<User, ForbiddenTransition>;

Backed by an immutable account_state_transitions audit table; each transition publishes a NATS event (account.suspended, account.restored, …) so downstream services react automatically — cancel open orders, freeze custody, notify the user.

§ 6 · Delivery

Milestone Placement

Three-way split aligned with existing contract milestones

M1 amendment
Foundations
~7d
FSM library + 4 EOD steps
  • FSM lib + audit table + 3→8 state migration
  • EOD step 01 — balance snapshot
  • EOD step 02 — P&L calculation
  • EOD step 03 — fee accrual to ledger
  • EOD step 04 — volume rollup
  • EOD step 06 — ME ↔ DB recon
  • Backs the M1 ledger acceptance gate (currently 🟡)
M4 — compliance
Lifecycle Hooks
~3d
FSM × KYC integration
  • EOD step 09 — auto-suspend on sanctions hit
  • EOD step 10 — reactivation on KYC re-verify
  • EOD step 11 — admin balance adjustments
  • FSM event subscription to KYC provider
  • Audit trail surface in admin panel
M5 — utility
Reconciliation & Distribution
~3d
External-boundary recon
  • EOD step 05 — staking reward distribution
  • EOD step 07 — DB ↔ custody recon (BitGo)
  • EOD step 08 — DB ↔ on-chain recon (staking)
  • Revenue-router gated payout

Why this split

The M1 amendment is the natural home for the four EOD steps that close existing yellow gates on Richard's M1 acceptance checklist (the ledger / reconciliation gates). The FSM ships in M1 too because every downstream milestone — KYC in M4, staking in M5, margin in M8 — depends on it.

External-boundary reconciliation (custody, on-chain) lands in M5 because both endpoints only exist by then: BitGo is wired in M2 and the staking contract deploys in M5.

§ 7 · Open Questions

Decisions Needed from QT Labs

Each blocks a portion of the work above

#QuestionBlocks
Q1Is there a longer system spec or operations manual we haven't been sent? The 8-page system spec PDF appears truncated — no buyback section, no technical-requirements section, no EOD section.All EOD scope
Q2What is the EOD cutover time? UTC 00:00 is the industry default but some venues use 16:00 NY (CME-style) or 08:00 UTC (Asia-handover). Affects fee-window boundaries.EOD step 04
Q3Tolerance for DB ↔ custody recon mismatch — what counts as "expected drift" (gas fees, network confirmations) vs. "page on-call"?EOD step 07
Q4For sanctions hits via the screening provider — is auto-suspend the desired action, or auto-restrict (withdrawal-only) so legitimate users aren't trapped?EOD step 09
Q5Should account closure (user-initiated) require a 30-day cooling-off period before terminal? Common compliance pattern; user can reverse during the window.FSM transition