delegation
A bounded permission transfer from a registered human to an agent wallet. Every scope is a PDA with four hard constraints. Up to five active scopes per writ. Revocable in one transaction.
EnoPMLDuLo33PUvYBekpaTzyembPuZD82PAcv3qvRFxKWhy five
The cap of five concurrent delegations is a policy number, not a cryptographic one. It bounds the attacker's gain from a single compromised identity. A human with five active scopes loses at most the union of their five budgets if every agent is captured simultaneously. Increasing the cap widens the blast radius of one compromised human; decreasing it hurts usability for power users who run multiple agents.
State
#[account]
#[derive(InitSpace)]
pub struct Scope {
pub writ: Pubkey, // 32
pub agent: Pubkey, // 32
pub programs: [Pubkey; 8],// 256 — zero pubkey = slot empty
pub budget_lamports: u64, // 8
pub spent_lamports: u64, // 8
pub expires_at: i64, // 8 — unix seconds
pub actions: u16, // 2 — bitmask
pub revoked: bool, // 1
pub index: u8, // 1 — slot in [0, 5)
pub bump: u8, // 1
}Action bitmask
The actions field encodes what kinds of operations the agent may perform. Bits are ORed; setting bit n authorizes action n.
| Bit | Action | Typical use |
|---|---|---|
| 0 | SWAP | DEX token swaps |
| 1 | PROVIDE_LIQUIDITY | AMM LP, CLOB market make |
| 2 | BORROW | money market draws |
| 3 | REPAY | money market repay |
| 4 | STAKE | validator / LST staking |
| 5 | UNSTAKE | unstake / unbond |
| 6 | NFT_BUY | marketplace purchase |
| 7 | NFT_SELL | listing / delist / transfer |
| 8 | BRIDGE | cross-chain outflow |
| 9 | COMPUTE_PAY | agent compute marketplace |
| 10–15 | CUSTOM_* | reserved for program-defined actions |
Instructions
create_delegation
Called by the human (writ authority) to initialize a new scope in one of the five slots. If all slots are full, the caller must first revoke one.
pub fn create_delegation(
ctx: Context<CreateDelegation>,
slot_index: u8, // 0..=4
agent: Pubkey,
programs: [Pubkey; 8],
budget_lamports: u64,
duration_seconds: u32, // expires_at = now + duration
actions: u16,
) -> Result<()>duration_seconds is capped at 7,776,000 (90 days). Longer delegations must be renewed — by design, so dormant permissions cannot silently persist.revoke_delegation
Sets revoked = true. The scope remains on-chain for audit purposes but all reads via writ_gate return NoActiveDelegation for this slot.
pub fn revoke_delegation(
ctx: Context<RevokeDelegation>,
slot_index: u8,
) -> Result<()>record_spend
Called by writ_gate during a successful verify_and_record to charge spent_lamports against the scope budget. This is the only instruction that mutates scope state after creation.
pub fn record_spend(
ctx: Context<RecordSpend>,
amount: u64,
) -> Result<()>The instruction is permissioned — only writ_gate can call it. Anchor enforces this via a constraint = program.key() == WRIT_GATE_ID on the RecordSpend context.
PDA derivation
let (scope_pda, bump) = Pubkey::find_program_address(
&[
b"delegation",
writ_account.key().as_ref(),
&[slot_index],
],
&delegation_program_id,
);Errors
| Code | When |
|---|---|
SlotOccupied | create_delegation into a slot where an unrevoked scope already exists |
NotWritAuthority | caller is not the authority on the parent WritAccount |
DurationTooLong | duration_seconds > 7,776,000 |
AlreadyRevoked | revoke_delegation on a scope already marked revoked |
BudgetOverflow | record_spend would overflow u64 |
Unauthorized | record_spend caller is not writ_gate |
Example: create a one-week LP delegation
const ix = await program.methods
.createDelegation(
0, // slot 0
agentKeypair.publicKey,
[RAYDIUM, ORCA, METEORA, ZERO, ZERO, ZERO, ZERO, ZERO],
new BN(50 * LAMPORTS_PER_SOL),
60 * 60 * 24 * 7, // 7 days
0b10, // PROVIDE_LIQUIDITY only
)
.accounts({ writ, scope })
.instruction();