Skip to content

Wallet Service

The Wallet Service is a FastAPI (Python) microservice that serves as the single custodian of all user balances (INR). It maintains a double-entry ledger, handles margin locking for orders, processes deposits/withdrawals, and exposes a high-performance gRPC API for internal services.

  • Balance Custody: Maintains per-user, per-currency balance slices: available, locked_order_margin, locked_withdrawal.
  • Double-Entry Ledger: Every balance mutation produces a debit + credit journal entry. The ledger is append-only and immutable.
  • Margin Locking: Lock funds when orders are accepted, unlock on cancel/reject, settle on fill.
  • Deposit / Withdrawal Processing: Handle deposit confirmations and withdrawal requests with multi-stage approval.
  • Idempotent Operations: Every mutating RPC accepts Idem { key, scope } — duplicates are safely rejected.
  • Hold System: Freeze funds under AML, risk, velocity, or operations holds.
  • FX Oracle: Provides USDT-INR conversion rate for settlement calculations.

Wallet does not decide whether to lock or unlock funds — it executes instructions from the Order Service, Settlement Service, and Admin Panel.

graph TB OrderSvc[Order Service] -->|gRPC| Wallet[Wallet Service] PositionSvc[Position Service] -->|gRPC| Wallet FundingSvc[Funding Service] -->|gRPC| Wallet AdminPanel[Admin Panel] -->|REST| Wallet Wallet -->|Stores| PG[(PostgreSQL)] Wallet -->|Events| Kafka[Kafka Bus] Wallet -->|Cache| Redis[(Redis)]
FieldTypeDescription
idUUID (PK)Balance row ID
user_idUUIDAccount owner
currencyVARCHAR(10)INR or USDT
cash_totalNUMERIC(20,4)Total cash balance
availableNUMERIC(20,4)Available for trading/withdrawal
locked_order_marginNUMERIC(20,4)Margin locked by open orders
locked_withdrawalNUMERIC(20,4)Funds locked for pending withdrawals
rounding_carryNUMERIC(20,8)Sub-paisa rounding accumulator
balance_versionBIGINTOptimistic locking version (incremented on every update)
created_atTIMESTAMPTZCreation time
updated_atTIMESTAMPTZLast update time

Invariant: cash_total = available + locked_order_margin + locked_withdrawal (enforced by CHECK constraint)

FieldTypeDescription
idUUID (PK)Ledger entry ID
user_idUUIDAccount owner
entry_typeENUMlock, unlock, settle_trade, deposit, withdrawal, funding, fee, admin_adjust
debit_accountTEXTChart of accounts debit (e.g., User:Cash)
credit_accountTEXTChart of accounts credit (e.g., Exchange:FeeRevenue)
amountNUMERIC(20,4)Entry amount (always positive)
currencyVARCHAR(10)INR or USDT
fx_usdtinrNUMERIC(12,4)FX rate at time of entry (NULL for INR-only)
reference_typeTEXTorder, trade, deposit, withdrawal, funding, admin
reference_idUUIDFK to the originating entity
trace_idTEXTOpenTelemetry trace ID
idempotency_keyTEXT UNIQUEDeduplication key
created_atTIMESTAMPTZEntry timestamp
AccountDescription
User:CashUser’s available cash
User:LockedMarginMargin held for open orders
User:LockedWithdrawalFunds pending withdrawal
Exchange:FeeRevenueTrading fee income
Exchange:FundingPoolFunding payment clearing
Exchange:InsuranceFundInsurance fund balance
Exchange:OperatingAccountExchange operating account
FieldTypeDescription
idUUID (PK)Hold ID
user_idUUIDAffected user
hold_typeENUM(AML, COOL_OFF, RISK, VELOCITY, OPS)Reason for hold
amountNUMERIC(20,4)Amount frozen (NULL = entire balance)
currencyVARCHAR(10)Currency
reasonTEXTHuman-readable reason
placed_byTEXTService or admin that placed the hold
released_atTIMESTAMPTZNULL until released
created_atTIMESTAMPTZHold creation time
FieldTypeDescription
idUUID (PK)Transaction ID
user_idUUIDAccount owner
amountNUMERIC(20,4)Transaction amount
currencyVARCHAR(10)Currency
statusENUMpending, processing, completed, failed, cancelled
payment_refTEXTExternal payment reference
created_atTIMESTAMPTZRequest time
completed_atTIMESTAMPTZCompletion time
FieldTypeDescription
keyTEXT (PK)Idempotency key
scopeTEXTOperation scope
response_hashTEXTHash of the original response
created_atTIMESTAMPTZFirst request time
expires_atTIMESTAMPTZKey expiry (e.g., 24 hours)

The Wallet Service exposes a gRPC API consumed by internal services:

RPCDescriptionCalled By
LockFunds(user_id, amount, currency, order_id, idem)Reserve margin for a new orderOrder Service
UnlockFunds(user_id, amount, currency, order_id, idem)Release locked funds on cancel/rejectOrder Service
SettleTrade(maker_id, taker_id, amount, fee_maker, fee_taker, trade_id, idem)Apply realized P&L and fees after a fillOrder Service
ApplyFunding(user_id, amount, currency, funding_cycle_id, idem)Process perpetuals funding paymentFunding Service
DepositConfirmed(user_id, amount, currency, payment_ref, idem)Credit balance after deposit confirmationPayment Gateway
WithdrawRequest(user_id, amount, currency, destination, idem)Initiate withdrawalClient via Gateway
WithdrawComplete(withdrawal_id, status, idem)Mark withdrawal as completed/failedPayment Gateway
AdminAdjust(user_id, amount, currency, reason, admin_id, idem)Manual balance adjustmentAdmin Panel
UpsertHold(user_id, hold_type, amount, currency, reason)Place a balance holdRisk / Admin
ReleaseHold(hold_id)Release a balance holdRisk / Admin
GetBalances(user_id)Query current balancesOrder Service, Position Service

Every mutating RPC includes Idem { key: string, scope: string }:

  1. On first call: execute operation, store result keyed by (key, scope)
  2. On duplicate call: return stored result without re-executing
  3. Keys expire after 24 hours
MethodEndpointDescription
GET/v1/wallet/balancesGet user balances
GET/v1/wallet/transactionsTransaction history (paginated)
POST/v1/wallet/depositInitiate deposit
POST/v1/wallet/withdrawRequest withdrawal
GET/v1/wallet/withdrawalsWithdrawal history
TopicDescriptionConsumers
wallet.ledger_posted.v1Every ledger entry writtenAnalytics, Audit, Settlement
wallet.balance_changed.v1Balance snapshot after changeRisk, Position, Frontend
wallet.deposit.confirmed.v1Deposit completedNotification Service
wallet.withdrawal.requested.v1Withdrawal initiatedRisk (AML check)
wallet.withdrawal.completed.v1Withdrawal finalizedNotification Service
wallet.hold.created.v1Balance hold placedNotification Service
wallet.hold_released.v1Balance hold releasedNotification Service
wallet.admin_adjust.v1Admin adjustment madeAudit Service
  1. Order Service calls LockFunds(user_id, margin_amount, INR, order_id)
  2. Wallet checks: available >= margin_amount and balance_version matches
  3. Atomically: available -= margin_amount, locked_order_margin += margin_amount, balance_version++
  4. Write ledger_entries (debit User:Cash, credit User:LockedMargin)
  5. Emit wallet.balance_changed.v1
  6. Return success
  1. Order Service calls SettleTrade(maker_id, taker_id, pnl, fees, trade_id)
  2. For each party:
    • Unlock margin for the filled portion
    • Apply realized P&L (credit or debit User:Cash)
    • Debit trading fee, credit Exchange:FeeRevenue
  3. All operations within a single DB transaction
  4. Write ledger entries for each leg
  5. Emit wallet.ledger_posted.v1 for each entry
  1. External payment gateway confirms deposit
  2. Gateway calls DepositConfirmed(user_id, amount, INR, payment_ref)
  3. Wallet credits available += amount
  4. Write ledger entry (debit Exchange:OperatingAccount, credit User:Cash)
  5. Emit wallet.deposit.confirmed.v1
  1. User requests WithdrawRequest(user_id, amount, INR, destination)
  2. Validate: available >= amount, no active holds blocking withdrawal
  3. Lock: available -= amount, locked_withdrawal += amount
  4. Apply cooling-off hold if configured
  5. Process via payment gateway
  6. On success: locked_withdrawal -= amount, write ledger entry
  7. On failure: reverse lock, restore available
  • Per-User Serialization: All balance mutations for a user are serialized via PostgreSQL advisory locks (pg_advisory_xact_lock(user_id_hash))
  • Optimistic Locking: Every balance update checks and increments balance_version. Concurrent updates fail and retry.
  • Idempotency: (key, scope) uniqueness in idempotency_keys prevents double-execution
  • Transaction Isolation: READ COMMITTED with explicit row locks for critical paths

The Wallet Service provides USDT-INR conversion for settlement:

  • Strategy: Composed index from multiple sources (exchange APIs, reference rates)
  • Staleness: FX rate has a configurable staleness threshold (e.g., 60 seconds). If stale, operations requiring FX conversion are rejected.
  • Recording: Every ledger entry involving cross-currency records the fx_usdtinr rate used
  • Rounding: Banker’s rounding (round half to even) for INR amounts to 4 decimal places
CurrencyPrecisionRounding
INR4 decimal placesBanker’s rounding
USDT4 decimal placesBanker’s rounding

Sub-paisa residuals accumulate in rounding_carry and are periodically reconciled.

  • Cooling Period: Configurable delay after deposit before withdrawal (e.g., 24 hours)
  • Daily Limits: Per-user daily withdrawal limits based on KYC tier
  • Velocity Checks: Max N withdrawals per hour
  • AML Screening: Large withdrawals trigger Risk Service review
  • Address Whitelisting: Optional destination address whitelist
  • wallet_lock_total{status} — Lock operations (success/failure/insufficient)
  • wallet_settle_total{status} — Trade settlements
  • wallet_deposit_total{status} — Deposit operations
  • wallet_withdrawal_total{status} — Withdrawal operations
  • wallet_balance_invariant_violations — Balance constraint violations (should be 0)
  • wallet_grpc_latency_ms{method} — gRPC method latency

Spans: wallet.lock_funds, wallet.unlock_funds, wallet.settle_trade, wallet.apply_funding, wallet.deposit, wallet.withdraw

Structured JSON: user_id, operation, amount, currency, balance_version, trace_id, idempotency_key. Amounts are logged; no PII.

  • gRPC Authentication: mTLS between services; service identity validated
  • Idempotency: Prevents replay attacks and double-spends
  • Advisory Locks: PostgreSQL advisory locks prevent concurrent balance races
  • Audit Trail: Every balance change has a corresponding immutable ledger entry
  • Hold System: Funds can be frozen without user’s ability to withdraw
  • Admin Operations: Require admin JWT scope + audit log entry
  • Financial Precision: NUMERIC(20,4) for all monetary values (never floating-point)
ScenarioHandling
DB transaction failureAutomatic rollback; client retries with same idempotency key
Kafka publish failureTransactional outbox pattern: ledger entry + outbox row in same transaction
FX rate staleReject cross-currency operations; alert ops
Payment gateway timeoutWithdrawal stays in processing; background poller checks status
Balance invariant violationAlert critical; pause mutations for user; trigger reconciliation
MetricTarget
LockFunds latency (p95)< 50 ms
SettleTrade latency (p95)< 100 ms
GetBalances latency (p95)< 20 ms
Balance invariant accuracy100%
Idempotency correctness100%
Ledger consistency (audit)+/- 0.01%
VariableDescriptionDefault
POSTGRES_URLPostgreSQL connection stringRequired
REDIS_URLRedis connection stringRequired
KAFKA_BROKERSKafka broker addressesRequired
GRPC_PORTgRPC server port50051
FX_STALENESS_THRESHOLD_SECMax FX rate age60
WITHDRAWAL_COOLING_HOURSPost-deposit cooling period24
MAX_DAILY_WITHDRAWAL_INRDaily withdrawal limitTier-based
  • Language: Python 3.10+
  • Framework: FastAPI + Uvicorn
  • ORM: SQLAlchemy + Alembic
  • gRPC: grpcio + protobuf
  • Kafka: confluent-kafka-python with Avro
  • Package Manager: uv