Paystack is a custodial escrow protocol. It is defined by a single invariant: no money leaves the buffer without a matching hold, a valid receipt, and an elapsed audit window. The Ledger records events. The Verifier enforces the invariant.
Every operation — hold, release, reverse, refund — writes a new entry. Nothing is edited. Nothing is deleted. Each entry carries the hash of its parent, forming a chain that the Verifier can walk end-to-end.
type LedgerEntry = {
id: UUIDv7 // monotonically sortable
parent_id: UUIDv7 | null // forms a hash chain
op: 'hold' | 'release' | 'reverse' | 'refund'
amount_cents: i64
currency: 'USD'
counter_party: string // stable vendor handle
vfp: string // public receipt id
state: 'pending' | 'cleared' | 'void'
reasoning: ReasoningEnvelope // see /docs
hash: sha256 // H(parent.hash || body)
signed_at: iso8601
}The Verifier runs out-of-process and has read-only credentials. It cannot alter a single cent. Its job is to look for breaks in the chain, releases without holds, and releases before the audit window has elapsed.
If it finds one, it pages the on-call engineer, freezes downstream releases, and emits a public incident on the Trust Center. You find out before your treasury does.
See incident process →// Verifier runs out-of-band. It never mutates the ledger.
// It proves that every release has (a) an earlier hold,
// (b) a valid receipt, and (c) an elapsed audit window.
for entry in ledger.walk(from: checkpoint) {
if entry.op == 'release':
hold = ledger.find(vfp: entry.vfp, op: 'hold')
receipt = receipts.find(vfp: entry.vfp)
assert hold // no release without a hold
assert receipt and receipt.valid // vendor confirmed work
assert now - hold.signed_at >= audit_window // buffer elapsed
assert entry.hash == sha256(parent || body) // chain intact
}
checkpoint.advance(entry.id)
anchor.publish(checkpoint.root) // notarized to a public anchor weeklyAn escrow transitions through a small number of states. The Verifier enforces that transitions only go one way, with the one exception allowed before release: reversal.
| State | Meaning | Exits to | Who triggers |
|---|---|---|---|
| held | Funds are in the FBO buffer against a VFP. | → released · → reversed · → refunded | Agent (hold) · Operator (reverse) |
| released | Funds have cleared to the counter-party. | terminal — dispute-only after this | Protocol (auto, T+buffer) |
| reversed | Hold was cancelled inside the audit window. | terminal — funds return to sender | Operator · SafetySwitch · Verifier |
| refunded | Vendor-initiated return after release. | terminal | Vendor |
Every release must reference a prior hold on the same VFP. Verifier enforced.
Audit window elapses before funds move. Enforced by protocol, not policy.
Any held escrow can be pulled back pre-release. One API call. No vendor involvement.
Compliance, finance, and engineering read the same ledger. No reconciliation.
Weekly root hash is notarized publicly. Tampering is externally detectable.
Cross-tenant reads and writes are architecturally impossible, not merely forbidden.