Real-time payments ·
Intraday liquidity you can enforce, not just report
Treasury usually learns the intraday position from a report. On an instant rail, the position has to be a limit the ledger enforces on every payment.
On a batch rail, treasury can manage intraday liquidity from reports: positions arrive on a cycle, and there is time between cycles to move money. On an instant rail, payments leave every second of every day. By the time a report says the position is low, the payments that made it low have already settled.
That changes what intraday liquidity has to be. Not a number someone watches, but a limit the ledger enforces on every payment, at the moment it posts.
Developer-preview note: every engine behaviour below was checked against a running engine. Threshold alerts, forecasting, automatic sweeps and payment queueing are not built; the last section is explicit about the boundary.
The position is an account
Model the participant’s settlement position for a rail — prefunded, collateralized, or capped — as a ledger account. Funding credits it. Every outbound payment debits it. Its limit_low is the floor the position may never cross:
c.create_account(AccountCreate(id=POSITION, ledger_id=P, currency=124,
limit_low=0, limit_high=10**15))
c.transfer(TransferCreate(source_id=9, ledger_id=P, currency=124,
debit_account_id=CENTRAL, credit_account_id=POSITION,
amount=1_000_000, txn_id="prefund-0800"))
The engine checks the floor in the same step as the write, one write per account at a time. Two payments that arrive in the same millisecond cannot both pass a check and jointly breach the floor. A payment that would cross it is refused with ERR_LIMIT — at the moment of the payment, not in a report afterwards — and the refusal is a durable record.
Reserve what you already owe
Some outflows are known in advance: a payroll file at 15:00, a settlement obligation, a large customer payment scheduled for the afternoon. Treasury wants that liquidity set aside so the stream of instant payments cannot consume it first.
That is a hold on the position account. The ledger’s reservation TTL releases it automatically if the obligation is never paid:
P = c.create_ledger(LedgerInstance(ledger_id=0, currency=124,
reserve_ttl_ns=4 * 3600 * 10**9)).ledger_id # holds expire after 4 h
c.reserve(TransferCreate(source_id=9, ledger_id=P, currency=124,
debit_account_id=POSITION, credit_account_id=OUT,
amount=600_000, txn_id="payroll-1500"))
# position: posted 1,000,000 — available 400,000
With 600,000 reserved, an instant payment of 500,000 is refused with ERR_LIMIT; one of 400,000 goes through and leaves exactly zero available. The reserved obligation is protected without anyone having to stop the rail.
Sponsors: a cap per client
A sponsor bank, or an aggregator giving smaller institutions access to a rail, carries a risk the rail does not see: one downstream client can consume the liquidity the sponsor has posted for all of them.
Give each client its own account with its own net debit cap:
c.create_account(AccountCreate(id=CLIENT_A, ledger_id=P, currency=124,
limit_low=-250_000, limit_high=10**15))
# client A has sent 200,000; a further 60,000 → ERR_LIMIT
c.update_account_limit_low(AccountKey(ledger_id=P, currency=124, id=CLIENT_A),
-300_000)
# cap raised intraday; the same 60,000 now posts, position -260,000
Each client’s exposure is a live balance. The cap is enforced on every payment, and changing it is a single ledger call that applies to the next write — no deployment, no restart. The FBO pattern keeps the sum of client positions equal to the sponsor’s pooled account by double entry.
The intraday curve, from the lake
Enforcement is half the job. The other half is evidence: what was the position at every moment of the day, when did it peak, and how much liquidity was actually used? Those are the measures intraday liquidity monitoring asks for — the Basel Committee’s intraday monitoring tools (BCBS 248) use this kind of time series.
With full_txn_record enabled, the Iceberg exporter writes both accounts’ post-apply balances onto every exported row. The position’s whole intraday curve is then a query over your transactions table:
SELECT ts, balance FROM (
SELECT global_batch_seq, array_index, timestamp AS ts,
dr_credits_posted - dr_debits_posted AS balance
FROM transactions WHERE debit_account_id = '8001' AND result_code = 0
UNION ALL
SELECT global_batch_seq, array_index, timestamp,
cr_credits_posted - cr_debits_posted
FROM transactions WHERE credit_account_id = '8001' AND result_code = 0
) ORDER BY global_batch_seq, array_index; -- the ledger's commit order
The minimum of that series is the day’s peak usage; its timestamp is when it happened; dr_available and cr_available show the same curve net of reserved obligations. Because the export is an ordinary Iceberg table, the same data can feed treasury’s dashboards, and supply the time series that intraday monitoring and regulatory reporting draw on.
Two operational notes. full_txn_record is off by default because it adds columns to the table schema, so it needs a fresh table. And it reports exactly what the ledger committed — it is evidence of the position, not a model of it.
What the engine does not do
The engine enforces and records. It does not yet decide:
- No alerts or forecasts. Thresholds, early-warning levels and liquidity forecasts belong to your monitoring today, fed by the ledger’s metrics and export.
- No automatic sweeps. The ledger records a transfer between two rails’ settlement positions exactly; actually moving funds between settlement accounts is outside it.
- No queueing. At the floor, a payment is refused, not queued for later release. Liquidity-saving mechanisms that queue and offset payments are not built.
Those are the next layer up, and we would rather build them with participants who run them than guess. If intraday liquidity on an instant rail is your problem, become a design partner.
Preparing for an instant rail?
Prove a real-time posting layer in shadow mode on replayed or anonymised traffic, before anything depends on it. We reply within one business day.