Participants
| Role | Responsibility |
|---|---|
| Client | Funds the escrow, signs payment authorizations off-chain with a session key. |
| Middleware | Generates payment requirements, gates the resource, and negotiates a hold with the client. |
| Facilitator | Validates authorizations, runs settlement policy, and submits authorizations on-chain. |
Escrow accounts
An escrow is a prepaid balance held by the on-chain program on behalf of a single client and a single facilitator. It can hold multiple token mints. Each escrow is parameterized by:refund_timeout_slots— how long a pending settlement waits before finalizing. Bounded between ~150 slots (~1 minute) and ~1,296,000 slots (~6 days).deadman_timeout_slots— how long the facilitator can be inactive before the client can unilaterally close. Bounded between ~1,000 slots (~6.7 minutes) and ~2,592,000 slots (~12 days). Must be at least2 * refund_timeout_slots.max_session_keys— caps the number of registered session keys.0means unlimited.
index, so a single client may hold multiple escrows with the same facilitator.
Once created, the escrow contract requires dual authorization for any transfer out: the client’s session-key signature on a payment authorization, plus an on-chain transaction signature from the registered facilitator. Neither party can move funds unilaterally.
Session keys
Session keys are Ed25519 keypairs that the client registers on-chain to sign payment authorizations off-chain. They exist so that:- The client never needs to sign with the escrow account’s private key on every request.
- Smart wallets, multisigs, and custodial wallets work natively. None of them can sign arbitrary off-chain messages, but they can sign the on-chain transaction that registers a session key.
- Keys can be revoked unilaterally by the client. A configurable grace period (
revocation_grace_period_slots) lets the facilitator settle in-flight authorizations after revocation but before the key becomes fully invalid.
expires_at_slot.
Payment authorizations
A payment authorization is a client-signed message that authorizes a single payment from the escrow. The signed fields are:| Field | Description |
|---|---|
programId | The Flex program ID (binds the signature to a specific deployment). |
escrow | Escrow PDA the payment is drawn from. |
mint | SPL token mint to settle in. |
maxAmount | Ceiling the facilitator may settle up to. The actual settle_amount can be lower. |
authorizationId | Random u64 identifier; provides on-chain replay protection via PDA seeds. |
expiresAtSlot | Slot after which the authorization may not be submitted. |
splits | Recipient list with basis-point allocations summing to 10,000. |
Splits
Splits encode multi-recipient settlement directly into the signed authorization. Each entry is(recipient_token_account, bps) and the entries must sum to exactly 10000 (100%). A single-recipient payment is a one-entry split with 10000 bps.
Splits enable platform fees, referral commissions, royalties, and facilitator fees in a single atomic settlement. The on-chain program distributes funds proportionally at finalize time. The current Solana program supports up to 5 splits per authorization.
Holds
When a request arrives, the middleware and facilitator coordinate a hold before any work is done:- Hold request. The middleware tells the client the maximum amount the operation may consume.
- Client authorization. The client signs an authorization for
maxAmount(which may be larger than the requested amount, leaving headroom for variable cost). - Hold validation. The middleware forwards the signed authorization to the facilitator. The facilitator validates the signature, confirms sufficient escrow capacity, and reserves it in memory.
- Service delivery. With the hold reserved, the middleware performs the work.
- Settlement. The middleware reports the actual amount consumed. The facilitator converts the in-memory hold to an on-chain pending settlement at the actual amount used.
<= maxAmount) and turns the hold into a pending settlement.
Pending settlements and the refund window
Asubmit_authorization instruction creates a PendingSettlement PDA with:
amount— the actual amount being settled (the hold’ssettleAmount, not the ceiling).original_amountandmax_amount— preserved for audit.submitted_at_slotandexpires_at_slot.
- Refund window. Until
submitted_at_slot + refund_timeout_slotselapses, the facilitator may callrefundto reduce or cancel the amount. This is how the system handles failed deliveries or disputes the facilitator agrees with. - Finalize. After the refund window closes, anyone can call
finalizeto distribute funds per the signed splits and close the pending settlement PDA. The rent for the PDA is returned to the facilitator. - Void on deadman. If the deadman switch fires, all pending settlements are voided and the funds remain in the escrow.
MAX_PENDING_SETTLEMENTS = 16. Facilitators must finalize older settlements before submitting new ones once the cap is reached.
Deadman switch
If the facilitator becomes unresponsive — nosubmit_authorization, no refund — the client can recover funds without facilitator cooperation once last_activity_slot + deadman_timeout_slots has elapsed.
The recovery is a three-step sequence:
void_pendingfor each open pending settlement. Either the owner or the facilitator can call this once the deadman timer has expired (or once a pending settlement has been sitting pastsubmitted_at_slot + refund_timeout_slots + deadman_timeout_slots). Funds reserved behind the pending PDA return to the vault.revoke_session_key+close_session_keyfor each registered session key.emergency_closerequiressession_key_count == 0.emergency_close— owner-only. Drains all vault token accounts to owner-controlled destinations and closes the escrow PDA.
last_activity_slot is only updated by instructions that require the facilitator’s signature (submit_authorization, refund). Permissionless instructions like finalize and deposit do not reset the timer. This prevents a malicious facilitator from holding funds hostage by cranking finalizations while ignoring new business.
The deadman switch guarantees that funds are never permanently locked, regardless of facilitator behavior. Choose a deadman_timeout_slots that balances client patience against the facilitator’s legitimate downtime tolerance.
Replay protection and expiry
Each authorization carries a uniqueauthorization_id. The pending-settlement PDA seeds include this ID, so a duplicate submit_authorization fails at account initialization. expires_at_slot provides a second layer: an authorization that the facilitator delays past its expiry can no longer be submitted.
The on-chain program also bounds how far in the future an expiry may be: at submission time, expires_at_slot must be <= current_slot + escrow.refund_timeout_slots. This caps a stale authorization’s settlement window to the same horizon as the refund window.
Security model at a glance
| Compromised parties | Severity | What attacker can do |
|---|---|---|
| Middleware alone | None | Cannot sign; can only forward authorizations the client signed. |
| Session key alone | Low | Can sign authorizations, but cannot submit them without facilitator co-op. |
| Session key + facilitator | Critical | Can drain up to signed maxAmount per authorization via fraudulent splits. |
Further reading
- Quickstart — end-to-end client flow with
@faremeter/flex-solana. - Facilitator — operator-side hold management and settlement.
- Program Reference — on-chain instructions, account layouts, and constants.
- Payment Schemes — where Flex sits among Faremeter’s other schemes.