Overview
Edict is a Solana program that holds funds for an AI agent and moves them only when the owner’s rules allow it. The owner is your wallet. The agent has its own key, and that key can only ask: every move goes through Edict, which asks four questions in order and either makes the move, holds it, or refuses it.
- The funds sit at the vault’s authority address, which only the Edict program can sign for.
- The owner writes the rules and can always withdraw, even while the vault is frozen.
- A guardian (optional) is a second key that can freeze the vault and cancel holds, and nothing else.
Quickstart
- Create the vault from your wallet with
create_vault: agent key, guardian, hold delay and tripwire. - Write the rules with
set_rulefor each asset: per move, per 60 minutes, per 24 hours, and the hold line. - List what it may touch with
allow_programandallow_recipient. - Fund the vault: send SOL or tokens to the vault authority address.
- Hand the agent its key. From now on the agent calls Edict instead of calling Jupiter or paying wallets directly.
You can draft and test a rulebook first in the app: it runs the same rules in your browser. The program’s interface (IDL) is at /edict.json.
| Program ID | Edicttra2izwdGeK9dwwh7azWmUbaeTJCf6x8BcnDPXu · live on Solana mainnet · view on Solscan |
| Built with | Pinocchio, no framework at runtime · 62 KB · Anchor-compatible interface |
| Tests | 21 on LiteSVM, including a randomized month of moves and a move-by-move comparison with a reference Anchor build |
| Compute | transfer_sol about 7,400 units · call with a swap about 32,000 |
// the agent pays an allowed wallet
await edict.methods.transferSol(new BN(120_000_000))
.accounts({ agent, vault, authority, recipient, recipientEntry })
.rpc()
// the agent swaps: the Jupiter instruction goes through Edict.call
await edict.methods.call(jupiterIx.data)
.accounts({ agent, vault, authority, targetProgram: JUPITER, programEntry })
.remainingAccounts(jupiterIx.keys)
.rpc()The four questions
Every move asks, in this order:
- Is the vault open? A frozen vault moves nothing.
- Is the destination allowed? Payments only to listed wallets, calls only to listed programs.
- Is it within the caps? Per move, in any 60 minutes, in any 24 hours, for that asset.
- Is it over the hold line? Big transfers wait; the owner or guardian can cancel them.
If one answer is no, nothing moves. Assets without a rule can’t move at all.
Rolling windows
Fixed buckets (“per clock hour”, “per UTC day”) let an agent spend the cap just before the boundary and again just after it: twice the cap in two minutes. Edict keeps each asset’s spending in rings of slots instead: 13 slots of 5 minutes for the hour, 25 slots of 1 hour for the day. A spend is remembered for 60 to 65 minutes, and for 24 to 25 hours.
The guarantee: never more than the hourly cap in any 60 minutes, and never more than the daily cap in any 24 hours. The price of that guarantee is that a spend can stay counted up to five minutes (or one hour) longer than the window itself.
Holds
A transfer over the asset’s hold line is queued with queue_transfer instead of sent. It can be released after the hold delay with release_hold_sol or release_hold_token (by the agent or the owner), and it expires if nobody releases it within the hold’s time to live. The owner or the guardian can cancel it at any time with cancel_hold. At release, the day window is checked again and the amount counts toward both windows.
Freeze and tripwire
The owner or the guardian can freeze the vault in one transaction; only the owner can unfreeze. The tripwire freezes it on its own: when refused moves reach the set number inside the set window (for example three in ten minutes), the vault freezes.
That works because Edict refuses softly: a refused move doesn’t fail the transaction, it moves nothing, records the refusal and its reason (in a Refused event and in the transaction’s return data), and counts a strike. A failed transaction would undo the strike along with everything else. A refused release counts a strike only when the agent sent it.
Every step leaves an event: VaultCreated, RuleSet, Allowed, Revoked, Passed, Refused, Held, Released, Cancelled, Expired, Frozen (by owner, guardian or tripwire), Unfrozen, Withdrawn.
Program calls
call forwards an instruction to an allowed program, with the vault’s authority signing. Before the call, Edict finds every vault token account among the accounts passed and reads its balance, plus the authority’s SOL. After the call it checks that each of those accounts is still owned by the vault, has no delegate and no close authority, and still exists. Then it counts what left, per asset, against the caps. Any problem after the call undoes the whole transaction.
Counting by balance means the caps hold for any program, whatever its instruction format: a swap, a deposit, a perp order. A call can also never move more than the hold line: big moves only leave through a hold.
Because a call is measured after it runs, a call that breaks a cap is undone as a whole (the transaction fails with OverPerTx, OverHourly, OverDaily, OverHoldLine, ApprovalLeft or AuthorityChanged) and counts no strike. Refusals before the call (frozen vault, program not listed, a vault account without a rule) are soft, like transfers.
Instructions
| Instruction | Who | What it does |
|---|---|---|
| create_vault | owner | Creates the vault for an agent key, with an optional guardian, the hold delay and time to live, and the tripwire. |
| set_rule · remove_rule | owner | Sets or removes an asset’s caps: per move, any 60 minutes, any 24 hours, and the hold line. |
| allow_recipient · revoke_recipient | owner | Adds or removes a wallet the agent may pay. |
| allow_program · revoke_program | owner | Adds or removes a program the agent may call. System, Token, Token-2022, loaders and Edict itself are refused. |
| set_agent · set_guardian | owner | Rotates the agent key or the guardian. |
| set_holds · set_tripwire | owner | Changes the hold delay and time to live, or the tripwire. |
| freeze | owner, guardian | Freezes the vault: nothing moves. |
| unfreeze | owner | Opens the vault again and clears the strikes. |
| owner_withdraw_sol · owner_withdraw_token | owner | Pulls funds out to any address, even while frozen. The vault keeps its rent minimum. |
| transfer_sol · transfer_token | agent | Pays an allowed wallet, within the caps and under the hold line. For tokens, the allowed wallet is the owner of the destination token account. |
| queue_transfer | agent | Queues a transfer of SOL or a token over the hold line; it must fit the 24-hour cap. |
| release_hold_sol · release_hold_token | agent, owner | Sends a queued transfer once its delay has passed, before it expires. |
| cancel_hold | owner, guardian | Cancels a queued transfer. |
| expire_hold | anyone | Closes a queued transfer that nobody released in time. |
| call | agent | Calls an allowed program with the vault signing, then checks balances, approvals and owners, the caps and the hold line. |
Reason codes
| Code | Meaning |
|---|---|
| 0 | Passed |
| 1 | Vault frozen |
| 2 | Recipient not allowed |
| 3 | Program not allowed |
| 4 | No rule for this asset |
| 5 | Over the per-move cap |
| 6 | Over the 60-minute cap |
| 7 | Over the 24-hour cap |
| 8 | Over the hold line: queue it |
| 9 | Under the hold line: send it directly (no strike) |
| 10 | A vault token account in the call has no rule |
Limits
- Caps count raw token amounts per asset. Edict doesn’t know prices, so a cap of 500 USDC and a cap of 5 SOL are separate limits.
- Allowing a program means trusting it with up to your caps. A bad swap route inside an allowed program still counts against the caps, but Edict doesn’t judge its price.
- The owner key is the root of the vault. Keep it in a hardware wallet if you can.
- The program has a full test suite but no external audit yet. Start with small vaults.
Token
EDICT is the project’s coin on Solana, launched on Pump.fun. The program is free to use; holding EDICT is not required for anything.