Programmable Payments: How Money Logic Moves Into the Ledger
Ready to make your payments programmable?
Request a demo to learn how Formance can help you make payments programmable.
Ready to make your payments programmable?
Request a demo to learn how Formance can help you make payments programmable.
In 2020, Citibank meant to wire $7.8 million in interest to Revlon's lenders and sent approximately $894 million instead, the full outstanding principal. The safeguard that should have stopped it was a set of on-screen checkboxes in the loan application sitting in front of the ledger, and the worker configuring the transfer checked the wrong combination. The ledger itself had no opinion on whether the principal account should move.
The window between an application-layer check and the subsequent ledger write is where such failures occur. Call it the commit-time gap. Any payment system where the condition lives in service code and the posting lives in the ledger has this gap, and every failure mode covered here is a variant of it. Overdrafts under concurrency, duplicate batches, stranded mid-hop value, and reconciliation drift.
Programmable payments address these failures by moving the condition into the ledger itself, so the check and the posting commit as a single atomic operation. The check and the write commit together, or neither does.
Programmable payments are flows of funds in which money-movement conditions are enforced by the financial system itself, rather than by application code running in front of it. The condition belongs to the money's infrastructure.
A scripted payment evaluates the condition in application code and then calls the ledger as a separate operation. It also differs from programmable money, which is “digital money whose transfer rules (who can hold it, when it can move and what conditions trigger a payment) are enforced by the ledger that holds the balance”.
But a programmable payment makes the authorization and the commit a single operation, so no state exists where the check passed, but the posting didn't. That single-operation guarantee is what commit-time enforcement delivers.
Payment conditions can live in application code, in an on-chain smart contract, or in the core ledger's commit path, and the layer determines whether the check and the posting are atomic.
| Layer | Where the check runs | Atomic with the posting? | Failure mode |
| Application code | Service code, before the ledger API call | No | The commit-time gap: time-of-check-to-time-of-use (MITRE CWE-367); concurrent transactions pass the check and overdraw |
| On-chain smart contract | On the same chain as the assets | Only when both legs live on the same chain | Any fiat leg on an external rail (e.g., RTGS) breaks atomicity; oracles cannot restore it |
| Core ledger commit path | Inside the ledger's transaction commit, under the write lock | Yes (commit-time enforcement) | Correctness depends on the ledger enforcing balance and account-state guards inside the commit |
When a payment system checks a balance in application code before telling the ledger to post the transaction, a brief moment sits between the check and the write where things can go wrong. Consider two $500 transfers hitting a $500 account at the same time: both see the balance, both get approved, and both go through. The account ends up at negative $500. The check went through, but it didn't hold the funds while the posting caught up.
On-chain smart contracts can reliably enforce conditions, but only when all the assets involved reside on the same ledger. The moment one side of the transaction is regular fiat money moving through a bank rail, the smart contract can't reach it. Smart contracts also can't see off-chain activity on their own, so something else has to bridge the two sides after the fact, and that hand-off is where the gap reopens.
The core ledger layer closes the commit-time gap by enforcing conditions during transaction commit. Balance and account-state guards, including asset type checks, run under the same lock discipline as the write, so no interleaving of concurrent transactions can produce a posting that the condition would have rejected. This is commit-time enforcement in practice.
Three flows benefit most from commit-time enforcement: conditional revenue splits, multi-hop cross-border settlement, and embedded treasury automation.
Conditional revenue splits belong in a single atomic posting so that the platform fee, reserve hold, and merchant credit either all commit or none do. A marketplace records each inbound payment as a single orchestrated posting, eliminating three sequential API calls and the reconciliation step those calls require. Partial commits and retry problems appear in the sequential version as one leg can be recorded without the others.
Let’s look at this in practice with Numscript, Formance’s purpose-built language for money.
The account model is: held funds for order 88213 in @escrow:orders:88213:held, platform fee revenue in @platform:revenue:fees, and merchant funds in @merchants:4471:reserve and @merchants:4471:available.
The atomic version represents the entire split as a single commit. A $185,000 settlement for merchant 4471, with a 2% platform fee and a 5% rolling reserve:
// MARKETPLACE_SETTLEMENT
// Event: settle order 88213; platform keeps a 2% fee and a 5% rolling reserve
send [USD/2 18500000] (
source = @escrow:orders:88213:held
destination = {
2% to @platform:revenue:fees
5% to @merchants:4471:reserve
remaining to @merchants:4471:available
}
)
set_tx_meta("event_type", "marketplace_settlement")
set_tx_meta("order_id", "88213")
Either the full split happens, or nothing does. The fee cannot post without the merchant credit posting alongside it. The transient hold account is also a reconciliation anchor because it should return to zero after settlement, and any residual becomes a specific, queryable number teams can investigate before month-end.
Multi-hop cross-border settlement requires a single atomic commit that spans fiat and digital-asset balances, plus stable business references on every external leg so retries don't double-post. A payment moving from USD to EUR via an intermediate stablecoin leg requires consistent retry handling for each leg.
A rail failure after the EUR debit but before the USD credit can leave stranded mid-hop value sitting in the intermediate stablecoin account; a naive retry may then re-run the first leg. Double-entry keeps postings balanced, but retry deduplication still requires stable business references, as a duplicate posting can balance internally. The trial balance ties to zero while cash is overstated.
In a Formance implementation, Connectivity ingests and unifies data from the underlying PSPs, banks, and digital-asset rails into a single data model. At the same time, Flows handles retries and fallbacks for each external leg. Then, our core ledger records the resulting postings with stable business references, so each attempt traces back to the same business event.
While Formance itself does not move the funds, the connected providers do, and every leg lands in the ledger as a single system of record.
Embedded treasury automation replaces scheduled polling with event-triggered sweeps whose postings are guarded at commit time. Latency drops, and concurrent sweep attempts cannot post invalid entries. Schedule-based sweep jobs run at fixed intervals and force teams to trade latency against the risk of overlapping runs; an event-triggered workflow removes that trade-off.
Guarding the sweep at commit time makes the condition explicit: "when @platform:treasury:operating exceeds the threshold, move the excess to @platform:treasury:yield."
Using the commit-time enforcement approach prevents concurrent sweep attempts from creating invalid ledger entries because the Ledger evaluates posting constraints at commit time.
Moving money logic into the ledger is a six-step migration: name the accounts, inventory service-code checks, key every write, backfill opening balances, shadow-run before cutover, and cut over and decommission.
In most platforms today, money logic lives scattered across service code, middleware jobs, scheduled scripts, spreadsheets, and manual approvals, none of which the ledger sees at commit time. Each step either installs a ledger invariant or adds the operating discipline the ledgered flow needs on every write.
Map each flow of funds to explicit account paths (@merchants:4471:reserve, @platform:revenue:fees) and posting rules before writing any enforcement logic. This step installs the first invariant, zero-sum balance: every transaction's postings sum to zero, checked at write time. Don't plan to balance it later; a ledger that accepts unbalanced postings has already created money out of thin air.
Every if balance >= amount that runs before a ledger write is a race condition waiting on concurrency, and each one widens the commit-time gap.
Replace each one with a ledger-side constraint inside the transaction commit, which installs the second invariant, commit-time conditions: a ledger posting that violates the transaction's rules fails with the transaction, with no partial execution.
A reserve clause belongs here too, so a payout that would fail its reserve-account movement is rejected at commit, before it reaches reconciliation. This is where the account paths from Step 1 become executable rules. The constraint you're inventorying from service code becomes a posting rule the ledger enforces on every write.
Attach a stable business reference to every write so that retries of the same event resolve to a single posting rather than multiple. Retry safety means the same business event should carry the same reference or metadata on every attempt.
A fresh UUID generated on each retry makes each retry appear to your application and ledger queries as a distinct request, while stable business references let your workflow layer detect duplicate attempts and keep retries and external rail callbacks tied to the original event.
Load the legacy system's current account balances into the new ledger as a dated, distinctly tagged transaction set before any shadow comparison begins. Day-one balances in the new ledger must match the legacy path before double-writing starts.
Avoiding backfill of opening balances means every discrepancy that Step 5 surfaces is unexplained, because you can't tell the drift introduced by the new logic.
Run the new ledger in shadow mode (writing every money movement while product surfaces still read from the legacy path) until drift against the legacy path hits a preset go/no-go threshold, then flip traffic.
Append-only immutability makes shadow mode safe because committed postings never change, and corrections are forward reversals, so the shadow ledger is a trustworthy record from day one.
During that period, teams compare balances against the legacy path and log discrepancies until drift has been eliminated.
Shadow mode can surface discrepancies in legacy systems' old calculation logic that they may have hidden. Delaying the migration can make a ledger rebuild substantially harder, which is why teams move to double-entry accounting before reconciliation complexity has already grown.
Some discrepancies send you back to Step 1 or Step 2 because the new logic has correctly exposed a rule the legacy system was silently getting wrong. Set the go/no-go threshold in advance (a defined window with zero unexplained drift) so the decision to flip traffic isn't made ad hoc.
Cut over by turning off legacy writes, keeping the legacy path read-only for a fixed retention window, and running reconciliation until that window closes without new drift. Only then is the legacy system safe to retire.
Flipping traffic is one milestone because finishing means running the retention window clean. Treating one clean shadow-run as permanent proof is the failure mode this step exists to prevent.
Start the programmable payments migration with the single flow of funds where reconciliation errors recur most often, because recurring breaks are evidence the commit-time gap is still open on that flow.
Citibank's $894 million wire trace to the same architectural issue: conditions that lived somewhere the ledger couldn't enforce them. All three flows (marketplace splits, multi-hop cross-border settlement, and treasury sweeps) become programmable payments the same way, by moving the condition into the commit.
Rewriting a flow as a ledger-native constraint collapses the reconciliation step for that flow. When the payment rule and posting are committed as a single unit, there is no second record to match against.
Formance provides the modules to support that structure: Ledger for commit-time enforcement, Connectivity for unifying data across PSPs, banks, and digital-asset rails, and Flows for orchestrating retries and fallbacks for each external leg of a programmable payment. Together, they're what the six-step migration runs on.