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/flex-solana reference. Program ID: EcfUgNgDXmBx4Xns2qZLE54xpM7V1N6PL8MdDW1syujS For full source, see github.com/faremeter/flex/programs/flex.

Constants

ConstantValueNotes
MAX_SPLITS5Recipients per authorization.
MAX_MINTS8Distinct token mints per escrow.
MAX_PENDING16Concurrent pending settlements per escrow. Re-exported from the TypeScript SDK as MAX_PENDING_SETTLEMENTS.
MIN_REFUND_TIMEOUT_SLOTS150~1 minute at 400 ms/slot.
MAX_REFUND_TIMEOUT_SLOTS1,296,000~6 days.
MIN_DEADMAN_TIMEOUT_SLOTS1,000~6.7 minutes.
MAX_DEADMAN_TIMEOUT_SLOTS2,592,000~12 days.
deadman_timeout_slots >= 2 * refund_timeout_slots is enforced at create_escrow time.

PDAs

AccountSeedsAuthority
Escrow[b"escrow", owner, index]Owner + Facilitator
Vault (token acct)[b"token", escrow, mint]Program (PDA signer)
Session Key[b"session", escrow, session_key]Owner
Pending Settlement[b"pending", escrow, authorization_id]Facilitator
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:
FieldTypeOffsetSize
versionu881
ownerPubkey932
facilitatorPubkey4132
indexu64738

Account structures

Escrow

FieldTypeNotes
versionu8Schema version for future migrations.
ownerPubkeyClient.
facilitatorPubkeyThe single registered facilitator.
indexu64PDA disambiguator.
pending_countu64Open pending settlements.
mint_countu64Active vault token accounts.
refund_timeout_slotsu64Refund window duration.
deadman_timeout_slotsu64Inactivity timeout for emergency_close and void_pending.
last_activity_slotu64Updated by facilitator-signed instructions only.
max_session_keysu80 means unlimited.
session_key_countu8Includes revoked-but-not-closed keys.
bumpu8PDA bump.

Session Key

FieldTypeNotes
versionu8
escrowPubkeyParent escrow.
keyPubkeyEd25519 public key.
created_at_slotu64
expires_at_slotOption<u64>Optional auto-expiry.
activebool
revoked_at_slotOption<u64>Set by revoke_session_key.
revocation_grace_period_slotsu64Settlement window after revocation.
bumpu8

Pending Settlement

FieldTypeNotes
versionu8
escrowPubkey
mintPubkey
amountu64Current amount; reducible by refund.
original_amountu64The submitted settle_amount.
max_amountu64Client-signed ceiling (audit only).
authorization_idu64Replay protection (in PDA seeds).
expires_at_slotu64
submitted_at_slotu64Refund window starts here.
session_keyPubkeyThe signing key.
split_countu81..=MAX_SPLITS.
splits[SplitEntry; MAX_SPLITS]Fixed-size; first split_count entries are valid.
bumpu8
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:
ConstantWhen
FLEX_ERROR__AUTHORIZATION_EXPIREDcurrent_slot >= expires_at_slot at submission.
FLEX_ERROR__EXPIRY_TOO_FARexpires_at_slot > current_slot + refund_timeout_slots.
FLEX_ERROR__INSUFFICIENT_BALANCEVault balance can’t cover settle_amount.
FLEX_ERROR__INVALID_SIGNATUREEd25519 verify failed or message didn’t match recomputed authorization.
FLEX_ERROR__INVALID_ED25519_INSTRUCTIONThe preceding instruction is not a valid Ed25519 verify.
FLEX_ERROR__SESSION_KEY_EXPIREDSession key past expires_at_slot.
FLEX_ERROR__SESSION_KEY_REVOKEDSession key revoked and grace period elapsed.
FLEX_ERROR__SESSION_KEY_GRACE_PERIOD_ACTIVETried to close a revoked key while still in grace period.
FLEX_ERROR__SESSION_KEY_STILL_ACTIVETried to close a session key that hasn’t been revoked.
FLEX_ERROR__SESSION_KEY_LIMIT_REACHEDsession_key_count == max_session_keys.
FLEX_ERROR__PENDING_LIMIT_REACHEDpending_count == MAX_PENDING.
FLEX_ERROR__PENDING_SETTLEMENTS_EXISTTried to close an escrow with pending_count > 0.
FLEX_ERROR__MINT_LIMIT_REACHEDmint_count == MAX_MINTS.
FLEX_ERROR__REFUND_AMOUNT_ZEROrefund_amount == 0.
FLEX_ERROR__REFUND_EXCEEDS_AMOUNTrefund_amount > pending.amount.
FLEX_ERROR__REFUND_WINDOW_EXPIREDTried to refund after the refund window closed.
FLEX_ERROR__REFUND_WINDOW_NOT_EXPIREDTried to finalize before the refund window closed.
FLEX_ERROR__REFUND_TIMEOUT_TOO_SHORTrefund_timeout_slots < MIN_REFUND_TIMEOUT_SLOTS at create.
FLEX_ERROR__REFUND_TIMEOUT_TOO_LONGrefund_timeout_slots > MAX_REFUND_TIMEOUT_SLOTS at create.
FLEX_ERROR__DEADMAN_TIMEOUT_TOO_SHORTdeadman_timeout_slots < MIN_DEADMAN_TIMEOUT_SLOTS.
FLEX_ERROR__DEADMAN_TIMEOUT_TOO_LONGdeadman_timeout_slots > MAX_DEADMAN_TIMEOUT_SLOTS.
FLEX_ERROR__DEADMAN_TOO_CLOSE_TO_REFUNDdeadman_timeout_slots < 2 * refund_timeout_slots.
FLEX_ERROR__DEADMAN_NOT_EXPIREDemergency_close called before the deadman timeout elapsed.
FLEX_ERROR__VOID_CONDITION_NOT_METvoid_pending called outside both allowed windows.
FLEX_ERROR__INVALID_VOID_AUTHORITYvoid_pending signer is neither the escrow owner nor the facilitator.
FLEX_ERROR__SESSION_KEYS_EXISTemergency_close called while session_key_count > 0.
FLEX_ERROR__OWNER_ONLYAn owner-only instruction was signed by someone else.
FLEX_ERROR__SPLIT_CALCULATION_OVERFLOWSplit distribution math overflowed at finalize time.
FLEX_ERROR__SESSION_KEY_ALREADY_EXPIREDTried to use a session key past its expires_at_slot.
FLEX_ERROR__GRACE_PERIOD_EXCEEDS_REFUND_TIMEOUTrevocation_grace_period_slots >= refund_timeout_slots at registration.
FLEX_ERROR__FINALIZATION_DEADLINE_PASSEDTried to finalize outside the allowed deadline window.
FLEX_ERROR__SETTLE_AMOUNT_ZEROsettle_amount == 0.
FLEX_ERROR__SETTLE_EXCEEDS_MAXsettle_amount > max_amount.
FLEX_ERROR__INVALID_SPLIT_COUNTEmpty splits or more than MAX_SPLITS.
FLEX_ERROR__INVALID_SPLIT_BPSSplit bps don’t sum to 10,000.
FLEX_ERROR__SPLIT_BPS_ZEROA split entry has bps == 0.
FLEX_ERROR__DUPLICATE_SPLIT_RECIPIENTDuplicate recipient in splits — collapse with mergeSplits first.
FLEX_ERROR__INVALID_SPLIT_RECIPIENTRecipient is not a valid token account for the mint.
FLEX_ERROR__INVALID_TOKEN_ACCOUNT_PAIRA (vault, destination) pair in close_escrow failed validation.
FLEX_ERROR__DUPLICATE_ACCOUNTSDuplicate mint in close_escrow remaining accounts.
FLEX_ERROR__UNAUTHORIZED_FACILITATORSigner doesn’t match escrow.facilitator.

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