> ## Documentation Index
> Fetch the complete documentation index at: https://docs.faremeter.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Flex Program Reference

> On-chain Anchor program: instructions, accounts, PDAs, constants, and errors.

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](/api/payment-solana.src.Namespace.flex).

**Program ID**: `EcfUgNgDXmBx4Xns2qZLE54xpM7V1N6PL8MdDW1syujS`

For full source, see [github.com/faremeter/flex/programs/flex](https://github.com/faremeter/flex/tree/main/programs/flex).

## Constants

| Constant                    | Value     | Notes                                                                                                        |
| --------------------------- | --------- | ------------------------------------------------------------------------------------------------------------ |
| `MAX_SPLITS`                | 5         | Recipients per authorization.                                                                                |
| `MAX_MINTS`                 | 8         | Distinct token mints per escrow.                                                                             |
| `MAX_PENDING`               | 16        | Concurrent pending settlements per escrow. Re-exported from the TypeScript SDK as `MAX_PENDING_SETTLEMENTS`. |
| `MIN_REFUND_TIMEOUT_SLOTS`  | 150       | \~1 minute at 400 ms/slot.                                                                                   |
| `MAX_REFUND_TIMEOUT_SLOTS`  | 1,296,000 | \~6 days.                                                                                                    |
| `MIN_DEADMAN_TIMEOUT_SLOTS` | 1,000     | \~6.7 minutes.                                                                                               |
| `MAX_DEADMAN_TIMEOUT_SLOTS` | 2,592,000 | \~12 days.                                                                                                   |

`deadman_timeout_slots >= 2 * refund_timeout_slots` is enforced at `create_escrow` time.

## PDAs

| Account            | Seeds                                    | Authority            |
| ------------------ | ---------------------------------------- | -------------------- |
| 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:

| Field       | Type   | Offset | Size |
| ----------- | ------ | ------ | ---- |
| version     | u8     | 8      | 1    |
| owner       | Pubkey | 9      | 32   |
| facilitator | Pubkey | 41     | 32   |
| index       | u64    | 73     | 8    |

## Account structures

### Escrow

| Field                   | Type   | Notes                                                        |
| ----------------------- | ------ | ------------------------------------------------------------ |
| `version`               | u8     | Schema version for future migrations.                        |
| `owner`                 | Pubkey | Client.                                                      |
| `facilitator`           | Pubkey | The single registered facilitator.                           |
| `index`                 | u64    | PDA disambiguator.                                           |
| `pending_count`         | u64    | Open pending settlements.                                    |
| `mint_count`            | u64    | Active vault token accounts.                                 |
| `refund_timeout_slots`  | u64    | Refund window duration.                                      |
| `deadman_timeout_slots` | u64    | Inactivity timeout for `emergency_close` and `void_pending`. |
| `last_activity_slot`    | u64    | Updated by facilitator-signed instructions only.             |
| `max_session_keys`      | u8     | `0` means unlimited.                                         |
| `session_key_count`     | u8     | Includes revoked-but-not-closed keys.                        |
| `bump`                  | u8     | PDA bump.                                                    |

### Session Key

| Field                           | Type         | Notes                               |
| ------------------------------- | ------------ | ----------------------------------- |
| `version`                       | u8           |                                     |
| `escrow`                        | Pubkey       | Parent escrow.                      |
| `key`                           | Pubkey       | Ed25519 public key.                 |
| `created_at_slot`               | u64          |                                     |
| `expires_at_slot`               | Option\<u64> | Optional auto-expiry.               |
| `active`                        | bool         |                                     |
| `revoked_at_slot`               | Option\<u64> | Set by `revoke_session_key`.        |
| `revocation_grace_period_slots` | u64          | Settlement window after revocation. |
| `bump`                          | u8           |                                     |

### Pending Settlement

| Field               | Type                       | Notes                                              |
| ------------------- | -------------------------- | -------------------------------------------------- |
| `version`           | u8                         |                                                    |
| `escrow`            | Pubkey                     |                                                    |
| `mint`              | Pubkey                     |                                                    |
| `amount`            | u64                        | Current amount; reducible by `refund`.             |
| `original_amount`   | u64                        | The submitted `settle_amount`.                     |
| `max_amount`        | u64                        | Client-signed ceiling (audit only).                |
| `authorization_id`  | u64                        | Replay protection (in PDA seeds).                  |
| `expires_at_slot`   | u64                        |                                                    |
| `submitted_at_slot` | u64                        | Refund window starts here.                         |
| `session_key`       | Pubkey                     | The signing key.                                   |
| `split_count`       | u8                         | 1..=MAX\_SPLITS.                                   |
| `splits`            | \[SplitEntry; MAX\_SPLITS] | Fixed-size; first `split_count` entries are valid. |
| `bump`              | u8                         |                                                    |

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:

| Constant                                          | When                                                                     |
| ------------------------------------------------- | ------------------------------------------------------------------------ |
| `FLEX_ERROR__AUTHORIZATION_EXPIRED`               | `current_slot >= expires_at_slot` at submission.                         |
| `FLEX_ERROR__EXPIRY_TOO_FAR`                      | `expires_at_slot > current_slot + refund_timeout_slots`.                 |
| `FLEX_ERROR__INSUFFICIENT_BALANCE`                | Vault balance can't cover `settle_amount`.                               |
| `FLEX_ERROR__INVALID_SIGNATURE`                   | Ed25519 verify failed or message didn't match recomputed authorization.  |
| `FLEX_ERROR__INVALID_ED25519_INSTRUCTION`         | The preceding instruction is not a valid Ed25519 verify.                 |
| `FLEX_ERROR__SESSION_KEY_EXPIRED`                 | Session key past `expires_at_slot`.                                      |
| `FLEX_ERROR__SESSION_KEY_REVOKED`                 | Session key revoked and grace period elapsed.                            |
| `FLEX_ERROR__SESSION_KEY_GRACE_PERIOD_ACTIVE`     | Tried to close a revoked key while still in grace period.                |
| `FLEX_ERROR__SESSION_KEY_STILL_ACTIVE`            | Tried to close a session key that hasn't been revoked.                   |
| `FLEX_ERROR__SESSION_KEY_LIMIT_REACHED`           | `session_key_count == max_session_keys`.                                 |
| `FLEX_ERROR__PENDING_LIMIT_REACHED`               | `pending_count == MAX_PENDING`.                                          |
| `FLEX_ERROR__PENDING_SETTLEMENTS_EXIST`           | Tried to close an escrow with `pending_count > 0`.                       |
| `FLEX_ERROR__MINT_LIMIT_REACHED`                  | `mint_count == MAX_MINTS`.                                               |
| `FLEX_ERROR__REFUND_AMOUNT_ZERO`                  | `refund_amount == 0`.                                                    |
| `FLEX_ERROR__REFUND_EXCEEDS_AMOUNT`               | `refund_amount > pending.amount`.                                        |
| `FLEX_ERROR__REFUND_WINDOW_EXPIRED`               | Tried to `refund` after the refund window closed.                        |
| `FLEX_ERROR__REFUND_WINDOW_NOT_EXPIRED`           | Tried to `finalize` before the refund window closed.                     |
| `FLEX_ERROR__REFUND_TIMEOUT_TOO_SHORT`            | `refund_timeout_slots < MIN_REFUND_TIMEOUT_SLOTS` at create.             |
| `FLEX_ERROR__REFUND_TIMEOUT_TOO_LONG`             | `refund_timeout_slots > MAX_REFUND_TIMEOUT_SLOTS` at create.             |
| `FLEX_ERROR__DEADMAN_TIMEOUT_TOO_SHORT`           | `deadman_timeout_slots < MIN_DEADMAN_TIMEOUT_SLOTS`.                     |
| `FLEX_ERROR__DEADMAN_TIMEOUT_TOO_LONG`            | `deadman_timeout_slots > MAX_DEADMAN_TIMEOUT_SLOTS`.                     |
| `FLEX_ERROR__DEADMAN_TOO_CLOSE_TO_REFUND`         | `deadman_timeout_slots < 2 * refund_timeout_slots`.                      |
| `FLEX_ERROR__DEADMAN_NOT_EXPIRED`                 | `emergency_close` called before the deadman timeout elapsed.             |
| `FLEX_ERROR__VOID_CONDITION_NOT_MET`              | `void_pending` called outside both allowed windows.                      |
| `FLEX_ERROR__INVALID_VOID_AUTHORITY`              | `void_pending` signer is neither the escrow owner nor the facilitator.   |
| `FLEX_ERROR__SESSION_KEYS_EXIST`                  | `emergency_close` called while `session_key_count > 0`.                  |
| `FLEX_ERROR__OWNER_ONLY`                          | An owner-only instruction was signed by someone else.                    |
| `FLEX_ERROR__SPLIT_CALCULATION_OVERFLOW`          | Split distribution math overflowed at finalize time.                     |
| `FLEX_ERROR__SESSION_KEY_ALREADY_EXPIRED`         | Tried to use a session key past its `expires_at_slot`.                   |
| `FLEX_ERROR__GRACE_PERIOD_EXCEEDS_REFUND_TIMEOUT` | `revocation_grace_period_slots >= refund_timeout_slots` at registration. |
| `FLEX_ERROR__FINALIZATION_DEADLINE_PASSED`        | Tried to `finalize` outside the allowed deadline window.                 |
| `FLEX_ERROR__SETTLE_AMOUNT_ZERO`                  | `settle_amount == 0`.                                                    |
| `FLEX_ERROR__SETTLE_EXCEEDS_MAX`                  | `settle_amount > max_amount`.                                            |
| `FLEX_ERROR__INVALID_SPLIT_COUNT`                 | Empty splits or more than `MAX_SPLITS`.                                  |
| `FLEX_ERROR__INVALID_SPLIT_BPS`                   | Split bps don't sum to 10,000.                                           |
| `FLEX_ERROR__SPLIT_BPS_ZERO`                      | A split entry has `bps == 0`.                                            |
| `FLEX_ERROR__DUPLICATE_SPLIT_RECIPIENT`           | Duplicate recipient in splits — collapse with `mergeSplits` first.       |
| `FLEX_ERROR__INVALID_SPLIT_RECIPIENT`             | Recipient is not a valid token account for the mint.                     |
| `FLEX_ERROR__INVALID_TOKEN_ACCOUNT_PAIR`          | A `(vault, destination)` pair in `close_escrow` failed validation.       |
| `FLEX_ERROR__DUPLICATE_ACCOUNTS`                  | Duplicate mint in `close_escrow` remaining accounts.                     |
| `FLEX_ERROR__UNAUTHORIZED_FACILITATOR`            | Signer 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

* [Concepts](/flex/concepts) -- protocol-level model.
* [Quickstart](/flex/quickstart) -- end-to-end client flow.
* [Facilitator](/flex/facilitator) -- operator-side primitives.
* [API Reference](/api/payment-solana.src.Namespace.flex) -- the `@faremeter/payment-solana` Flex API.
