writ_gate
The only program external protocols ever touch. Exposes a read-only verify CPI that returns a 64-byte AgentStatus packing identity, scope, reputation, and expiry.
3tpfhT2m1vF7FCLsGazbEPFRiRnjgwk2CnC3yeonas7MAgentStatus
The return type of every verify call. Designed to fit in two 32-byte slots so it can be passed between CPI boundaries without heap allocation.
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy)]
pub struct AgentStatus {
pub is_valid: bool, // 1 — human-backed, SBT still held
pub has_scope: bool, // 1 — at least one active delegation
pub score: u16, // 2 — 0..=10_000
pub expires_at: i64, // 8 — earliest active scope expiry
pub scope: ScopeSummary, // 48 — packed programs mask + budgets
pub flags: u32, // 4 — reserved for v0.5 extensions
}
pub struct ScopeSummary {
pub programs_hash: [u8; 32], // 32 — hash of Scope.programs, for constant-time compare
pub budget_remaining: u64, // 8 — budget − spent
pub actions: u16, // 2
pub slot_index: u8, // 1 — which of 5 slots matched
pub _pad: u8, // 1
}Convenience methods on AgentStatus:
| Method | Returns | Meaning |
|---|---|---|
.expired(now) | bool | expires_at < now |
.allows_program(pid) | bool | pid hashes into programs_hash |
.within_budget(amount) | bool | amount ≤ budget_remaining |
.allows_action(bit) | bool | (actions >> bit) & 1 == 1 |
.require_ok() | Result<()> | succeed only if fully valid |
Instructions
verify
Read-only. Returns AgentStatus with every field populated.
pub fn verify(ctx: Context<Verify>) -> Result<AgentStatus>verify_strict
Equivalent to verify followed by .require_ok(). Errors instead of returning a status — useful when you only need the happy path and want to save a branch.
pub fn verify_strict(ctx: Context<VerifyStrict>) -> Result<()>verify_and_record
Atomically verifies and debits the scope budget. Used by callers that cannot separate the check from the charge (e.g. perp matching engines). Mutates delegation.record_spend via CPI.
pub fn verify_and_record(
ctx: Context<VerifyAndRecord>,
amount: u64,
) -> Result<AgentStatus>Accounts (Verify context)
| Account | Type | Writable | Signer |
|---|---|---|---|
agent | Signer | yes | |
writ_account | PDA<WritAccount> | ||
scope_slots[0..5] | PDA<Scope> | ||
reputation_account | PDA<ReputationAccount> | ||
sbt_token_account | Token-2022 Account | ||
clock | Sysvar |
Return type in Anchor CPI
Anchor's return_data mechanism passes AgentStatus back to the caller without account mutation. The calling program must call .get() on the CPI result to deserialize it.
let cpi_ctx = CpiContext::new(
ctx.accounts.writ_gate_program.to_account_info(),
writ_gate::cpi::accounts::Verify {
agent: ctx.accounts.agent.to_account_info(),
writ_account: ctx.accounts.writ_account.to_account_info(),
// ... scope slots, reputation, sbt, clock
},
);
let status: AgentStatus = writ_gate::cpi::verify(cpi_ctx)?.get();Errors
| Code | When |
|---|---|
WritNotFound | agent has no WritAccount |
SbtMissing | SBT token account is closed or has 0 balance |
NoActiveDelegation | all 5 scopes are revoked, expired, or empty |
ProgramNotAllowed | caller's program_id is not in Scope.programs |
BudgetExceeded | verify_and_record would exceed budget |
ActionNotPermitted | action bit unset in Scope.actions |
ClockSkew | Scope.expires_at < sysvar_clock.unix_timestamp |
Design notes
writ_gate is intentionally thin. It does not implement policy beyond the invariants encoded in L1–L3. It cannot be used to add new constraints (e.g. per-counterparty limits, rate limiting) without changes to the underlying programs. This is a deliberate tradeoff: the gate is the stable interface, and keeping it minimal keeps it auditable and cheap to call.