Skip to main content
The Flex on-chain program is an Anchor program deployed on Solana. This page is the canonical reference for its instructions, accounts, and constraints. For higher-level API, see the @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 numeric index, 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_SLOTS
  • MIN_DEADMAN_TIMEOUT_SLOTS <= deadman_timeout_slots <= MAX_DEADMAN_TIMEOUT_SLOTS
  • deadman_timeout_slots >= 2 * refund_timeout_slots
Vault token accounts are created lazily on first deposit per mint.

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_keys
  • revocation_grace_period_slots < escrow.refund_timeout_slots
Note: 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_PENDING
  • current_slot < expires_at_slot
  • expires_at_slot <= current_slot + escrow.refund_timeout_slots
  • settle_amount > 0 and settle_amount <= max_amount
  • Session key is active, or revoked and within grace period
  • splits is non-empty, <= MAX_SPLITS, bps > 0 per 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
Effects: creates a 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), OR
  • current_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).
The pending PDA closes (rent returned to the facilitator), 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_amount per 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 by deadman_timeout_slots.
  • Middleware compromise — middleware never signs payments and never holds keys. It can refuse to serve requests but cannot move funds.
Mitigations clients should apply:
  • 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