Blockchain balance is not product balance
A wallet can hold 10,000 USDC and still tell you almost nothing about your product. It does not know whether those funds belong to ten customers, one invoice, a treasury transfer, a refund return or an operational top-up.
Wallet balance answers “where are assets?”. The internal ledger answers “why does the product recognise this value?”.
What the internal ledger is for
The ledger creates a durable business interpretation of external payment evidence. A provider order or blockchain transaction proves that something happened outside your database. The ledger records what that event means inside your product.
Examples of ledger events:
PAYMENT_ACCEPTEDCUSTOMER_CREDITEDPAYMENT_FEE_RECORDEDREFUND_INITIATEDREFUND_COMPLETEDMANUAL_ADJUSTMENTTREASURY_TRANSFER_RECORDED
Do not model the ledger as one mutable balance field
A mutable user.balance += amount operation loses the reason, evidence and history behind the change. Prefer an append-only set of entries and derive balances from those entries or from a carefully maintained projection.
Recommended core entities
PaymentIntent
The expected business payment: customer, invoice/order, amount, currency, expiry and allowed rail.
ExternalPayment
The evidence object: provider order ID or blockchain transaction hash, network, asset, amount, timestamps and external state.
LedgerEntry
An immutable business value event with debit/credit direction, amount, currency/unit, reason code, reference to PaymentIntent and ExternalPayment, idempotency key and created timestamp.
BalanceProjection
A derived view used for performance. The projection can be rebuilt from ledger entries; it should not be the only evidence.
SettlementRecord
A separate object representing provider settlement, withdrawal or treasury movement. A customer payment may be valid before settlement completes.
Idempotency belongs at the ledger boundary
Duplicate callbacks and repeated chain observations are normal. The same external event must never create the same economic effect twice. Store a unique key such as provider:event:id or chain:txHash:logIndex:businessAction and enforce uniqueness transactionally.
Credit and settlement are different facts
Consider a managed provider flow. The provider can confirm the customer payment and your product can credit the order. The provider’s EUR settlement may occur later. If settlement is delayed, the customer payment does not become invalid. Your model should therefore separate paymentAcceptanceState from settlementState.
The same rule applies to custom wallets. A valid deposit can credit the customer while a later treasury sweep fails. The sweep is an operational movement, not a reason to reverse the original payment.
Refunds should create compensating entries
A refund does not delete the original credit. Create a new refund lifecycle and new ledger entries. This preserves a complete explanation of gross payment, fees, partial refunds and net retained value.
Multi-currency and stablecoin units
Decide which unit each ledger represents. For B2B invoicing you may keep a commercial ledger in EUR and store USDC payment evidence separately. For a native stablecoin product you may operate a USDC-denominated product ledger. Do not mix units in one balance without explicit conversion entries and rate evidence.
Suggested ledger entry shape
| Field | Purpose |
|---|---|
| entryId | Immutable identifier |
| accountId | Customer, merchant, clearing or fee account |
| direction | Debit / credit |
| amount + unit | Value represented by this ledger |
| reasonCode | PAYMENT, REFUND, FEE, ADJUSTMENT… |
| paymentIntentId | Business context |
| externalReference | Provider order/event or chain tx |
| idempotencyKey | Prevents duplicate economic effects |
| createdAt | Audit ordering |
Reconciliation is the proof that the ledger is correct
An internal ledger without external reconciliation can still drift. Scheduled jobs should compare ledger totals and entries against provider or chain evidence. The process is described in Stablecoin Payment Reconciliation.
Provider and custom wallet differences
With CoinGate, provider orders and ledger transactions become external evidence. With custom wallets, blockchain transactions and chain observations become external evidence. The internal ledger architecture remains conceptually the same.
What the ledger should not do
- It should not store private keys.
- It should not infer product credit directly from raw wallet balance.
- It should not overwrite history to “fix” mistakes.
- It should not collapse payment, treasury and settlement into one status.
- It should not pretend to replace statutory accounting.
Ledger invariants
A production ledger should enforce invariants in code and at the database level where possible. Examples: an external event can create a given economic effect only once; entries are immutable after posting; debits and credits use a known unit; adjustments reference the entry or event being corrected; and a payment cannot be credited before the acceptance policy is satisfied.
Account structure
Even if you do not implement a full accounting general ledger, explicit logical accounts make the model clearer. Typical accounts can include customer available balance, merchant clearing, provider clearing, fees, refunds and treasury. Moving value between logical accounts is easier to audit than updating several unrelated balance fields.
Concurrency and transactions
Payments are concurrent systems. Two callbacks can arrive at the same time, or a callback can race with a reconciliation job. Ledger posting should happen inside a database transaction with uniqueness constraints on idempotency keys. If a projection is updated, update it in the same transaction or through a replayable event pipeline.
Audit queries the ledger should answer
- Why was this customer credited?
- Which external transaction caused this entry?
- Has this payment been refunded, partially refunded or adjusted?
- Which entries contributed to the current product balance?
- Has the accepted payment been reconciled with settlement?
Conclusion
The internal ledger is the boundary that lets a product use multiple payment rails without losing business consistency. Build it once around explicit value events and the system can later accept CoinGate, direct USDC deposits, cards or bank transfers while keeping the same core business model.