@faremeter/payment-solana Flex reference.
Program ID: EcfUgNgDXmBx4Xns2qZLE54xpM7V1N6PL8MdDW1syujS
For full source, see github.com/faremeter/flex/programs/flex.
Constants
deadman_timeout_slots >= 2 * refund_timeout_slots is enforced at create_escrow time.
PDAs
index and authorization_id are both u64 little-endian.
Discovery
Because the escrow PDA includes a numericindex, clients cannot derive a single canonical address. Use getProgramAccounts with memcmp filters on the account data, or the helpers findEscrowsByOwner / findEscrowsByFacilitator. Byte offsets after the 8-byte Anchor discriminator:
Account structures
Escrow
Session Key
Pending Settlement
A
SplitEntry is { recipient: Pubkey, bps: u16 }.
Instructions
Account management
create_escrow(index, facilitator, refund_timeout_slots, deadman_timeout_slots, max_session_keys)
Signers: owner.
Constraints:
MIN_REFUND_TIMEOUT_SLOTS <= refund_timeout_slots <= MAX_REFUND_TIMEOUT_SLOTSMIN_DEADMAN_TIMEOUT_SLOTS <= deadman_timeout_slots <= MAX_DEADMAN_TIMEOUT_SLOTSdeadman_timeout_slots >= 2 * refund_timeout_slots
deposit(amount)
Signers: depositor (anyone).
Constraints: amount > 0; mint_count < MAX_MINTS when adding a new mint.
The vault PDA is created on first deposit per mint. The depositor pays the rent and forfeits it on close (rent goes to the owner). Deposits do not update last_activity_slot.
close_escrow
Signers: owner + facilitator.
Constraints: pending_count == 0. The instruction expects mint_count * 2 remaining accounts: pairs of (vault_pda, owner_destination_token_account). Each vault is fully drained to its destination, then closed; finally the escrow PDA itself is closed. Each pair is validated and duplicate mints are rejected.
emergency_close
Signers: owner.
The deadman switch: lets the owner unilaterally recover funds when the facilitator has gone dark. Callable when current_slot > last_activity_slot + deadman_timeout_slots AND pending_count == 0 AND session_key_count == 0.
The owner must clear pending settlements with void_pending and close session keys with revoke_session_key + close_session_key before this instruction will succeed. Same remaining-accounts shape as close_escrow: mint_count * 2 writable token accounts, alternating (vault, owner_destination) per mint.
Session keys
register_session_key(session_key, expires_at_slot, revocation_grace_period_slots)
Signers: owner.
Constraints:
max_session_keys == 0 || session_key_count < max_session_keysrevocation_grace_period_slots < escrow.refund_timeout_slots
session_key_count includes revoked-but-not-closed keys. Keys in their grace period still consume a slot.
revoke_session_key
Signers: owner.
Sets revoked_at_slot. Authorizations signed before revocation remain submittable until revoked_at_slot + revocation_grace_period_slots.
close_session_key
Signers: owner.
Constraints: key must be revoked and the grace period must have elapsed. Returns rent to the owner and decrements session_key_count.
Settlement
submit_authorization(mint, max_amount, settle_amount, authorization_id, expires_at_slot, splits)
Signers: facilitator (must match escrow.facilitator).
Validation:
pending_count < MAX_PENDINGcurrent_slot < expires_at_slotexpires_at_slot <= current_slot + escrow.refund_timeout_slotssettle_amount > 0andsettle_amount <= max_amount- Session key is
active, or revoked and within grace period splitsis non-empty,<= MAX_SPLITS,bps > 0per entry, sum to 10,000, all recipients unique- An Ed25519 verify instruction must precede this one in the transaction; the program checks that the signed message matches a recomputed
serializePaymentAuthorization(...)using the supplied parameters
PendingSettlement PDA seeded by (escrow, authorization_id) (init fails on duplicate IDs), increments escrow.pending_count, updates escrow.last_activity_slot.
refund(refund_amount)
Signers: facilitator.
Constraints: current_slot < submitted_at_slot + escrow.refund_timeout_slots; refund_amount > 0 and <= pending.amount.
Decreases pending.amount by refund_amount. If the amount drops to zero, closes the pending settlement (returning rent to the facilitator). Updates escrow.last_activity_slot.
finalize
Signers: anyone (permissionless crank).
Constraints: current_slot >= submitted_at_slot + escrow.refund_timeout_slots.
Distributes pending.amount from the vault to recipients per pending.splits (proportional to bps), closes the pending settlement, decrements escrow.pending_count. Returns the pending PDA’s rent to the facilitator. Does not update last_activity_slot — see “Activity tracking” below.
Remaining accounts: one writable destination token account per split entry, in the same order as pending.splits.
void_pending
Signers: owner OR facilitator (validated against escrow.owner / escrow.facilitator).
Closes a stuck pending settlement so the escrow can eventually be closed. Allowed when either:
current_slot > last_activity_slot + deadman_timeout_slots(the deadman has fired), ORcurrent_slot > submitted_at_slot + refund_timeout_slots + deadman_timeout_slots(the pending settlement has been parked past its useful life, even though the facilitator may still be otherwise active).
escrow.pending_count decrements, and the funds remain in the escrow vault. last_activity_slot is not updated.
Activity tracking
last_activity_slot is updated by exactly two instructions: submit_authorization and refund. Both require the facilitator’s signature, so the timer reflects genuine engagement. finalize (permissionless) and deposit (anyone) do not touch it. This ensures a malicious facilitator cannot keep the escrow alive by cranking finalizations while ignoring new business.
Errors
The program emits errors as Anchor custom error codes (offset 6000). The TypeScript SDK exports each as a constant (e.g.,FLEX_ERROR__SESSION_KEY_EXPIRED) and provides getFlexErrorMessage(code) for human-readable text. Notable errors:
Security
The on-chain program enforces all the invariants above. The honest-facilitator assumption only matters for what the facilitator chooses to submit: it can refuse to submit a valid authorization, but it cannot fabricate one. Splits, amounts, mints, and recipients are bound to the client’s signature. Failure modes worth thinking through:- Session key compromise alone — attacker can sign authorizations but not submit them. No funds at risk without facilitator cooperation.
- Session key + facilitator collusion — attacker can drain up to the signed
max_amountper authorization, into recipients the client signed for. - Facilitator goes dark — client recovers funds via
void_pending(per stuck pending settlement) →revoke_session_key+close_session_key(per registered key) →emergency_close, all gated bydeadman_timeout_slots. - Middleware compromise — middleware never signs payments and never holds keys. It can refuse to serve requests but cannot move funds.
- Verify all split recipients before signing (treat the splits vector as part of the contract you’re signing, not a facilitator parameter).
- Use short-lived session keys with
expires_at_slot. - Fund escrows in proportion to expected usage; rotate to a fresh escrow if a key is suspected compromised.
- Monitor on-chain pending settlements for the escrow with
findPendingSettlementsByEscrow.
Further reading
- Concepts — protocol-level model.
- Quickstart — end-to-end client flow.
- Facilitator — operator-side primitives.
- API Reference — the
@faremeter/payment-solanaFlex API.