A Solana program that enforces signed intents over SPL token accounts.
An intent states what may leave one token account and what must arrive in another. The owner signs it as text. Anyone can execute it. The program checks the balances afterwards and fails the transaction if they are wrong.
Open → any instructions → Close
Open verifies the signature, pulls up to the signed limit as the account's SPL delegate
and snapshots every balance the intent names. Close compares them to the snapshots.
Funds stay in the owner's wallet until Open, and nothing in between is trusted.
Solana Mandate v1
cluster: localnet
engine: Mand89p7P6okjEKQx2SpwDX6mdb5zAcdpshRafFtv7A
authority: A9XwnWUxXn1HH1MPzxe5MqfYaKvEHtdQMCoDd72QjPLN
[0] MAY TAKE: at most 8.000000 of mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v from 6NSx1jcpyqzHDFHwC7RXm4LZy53gNsMZWpV3P8vr8k4M at a time
[1] REQUIRES: FAUD3SfhKYyynnZsS8pKVgZ9ea5VQohFqpaxKpAwzEGA (mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v, owner GNxM82DJMja5ux5extFCEjbQ5C88hvcG7fvsiSCQumgs) gains at least 1.000000 for every 1.000000 taken by [0]
EXECUTOR: anyone
VALID: from 2026-10-01T08:16:00Z until 2027-10-01T08:16:00Z
REPLAY: refills over 30d
EPOCH: 0
This is a subscription: 8 USDC, refilling over 30 days, payable only to one account, for a
year. The text is an Offchain Message v1. The transaction carries the terms as 289 bytes
of binary; the program renders them back to this text and verifies Ed25519 over it
in-program. The text is the only thing a wallet has to show, and a byte of the terms
cannot change without changing it (every_byte_of_the_terms_is_visible_in_the_text).
Terms { authority, executor: any | key, not_before, not_after, replay, epoch, asserts }
Replay = once | fill | rate(period)
Assert { target, mint, owner, bound }
Bound = const | linear(t0, v0, t1, v1) | ratio(of, num, den)
An assert is a lower bound on how much a token account's balance changes between Open
and Close. A negative constant on the authority's own account is a spending limit, and
is the only thing that permits a pull. Anything else is an amount that must arrive.
| Replay | Limit applies |
|---|---|
once |
to a single execution |
fill |
in total, across partial fills |
rate |
to a budget that refills linearly over period seconds |
Bounds are summed per account across every intent in the transaction, so one deposit cannot satisfy two intents and two intents can settle against each other.
| # | Instruction | Signer | |
|---|---|---|---|
| 0 | CreateMandate |
authority | store terms on chain instead of signing them |
| 1 | CloseMandate |
authority | delete stored terms |
| 2 | BumpEpoch |
authority | revoke every signed intent |
| 3 | RevokeSigned |
authority | revoke one signed intent |
| 20 | Open |
executor | authenticate, pull, snapshot |
| 21 | Close |
executor | check, end the session |
| 22 | CloseState |
anyone | return the rent of an expired or revoked intent's state |
Open and Close must be top-level. The first Open reads the instructions sysvar and
requires exactly one Close after it.
Replay state lives in one PDA per signed intent, created by the executor on first use. It can be closed only once the intent can never verify again, so it cannot be deleted and replayed.
LiteSVM, whole transaction:
| CU | |
|---|---|
Stored intent, Open + Close |
10k |
Signed intent, Open + Close |
81k to 95k |
| Signed swap with the solver's transfer | 103k to 110k |
The signed path pays for SHA-512 and Ed25519 in-program. The spread is PDA bump search.
- Not audited. The invariants in SPEC.md §9 are not model-checked.
- The program is upgradeable and is the delegate of every account that enables it.
- A token account has one delegate. Any other
Approveon it disables its intents. - Token accounts only. Native SOL has to be wrapped.
- Enabling a token account is an SPL
Approve, which is a transaction. - Token-2022 transfer hooks are not forwarded.
- No wallet implements
solana:signMandate. Signing falls back to an offchain message.
cargo build-sbf --manifest-path program/Cargo.toml --features localnet
cargo test --workspace
npm install
npm run dev # examples/agents on a Surfpool mainnet fork
npm run dev:subscriptions # examples/subscriptions| Path | Holds |
|---|---|
program |
The engine |
packages/mandate-core |
Terms, validity rules, the canonical text and account layouts, shared by the program and every client |
packages/sdk |
@solana/kit builders plus mandate-core compiled to WebAssembly |
packages/wallet-standard |
solana:signMandate, the one feature a wallet adds |
examples |
The agent demo and the subscription site |
tests |
LiteSVM flows and the canonical-text tests |
SPEC.md is the standard. The code calls an intent a mandate.