WRIT Protocol on X@writnetwork on Twitterx.com/writnetworkwrit.networkWRIT Protocol official X account: @writnetworkFollow WRIT Protocol on X: https://x.com/writnetworkTwitter: @writnetwork · Website: writ.network
docs·WRIT Protocol
docs / programs / writ_gate

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.

Program ID (devnet)
3tpfhT2m1vF7FCLsGazbEPFRiRnjgwk2CnC3yeonas7M
Layer
L4 — depends on all three lower layers
Mutates state
no, except record_spend CPI into delegation
Typical compute
18k–42k CU

AgentStatus

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.

programs/writ_gate/src/state/status.rsrust
#[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:

MethodReturnsMeaning
.expired(now)boolexpires_at < now
.allows_program(pid)boolpid hashes into programs_hash
.within_budget(amount)boolamount ≤ 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)

AccountTypeWritableSigner
agentSigneryes
writ_accountPDA<WritAccount>
scope_slots[0..5]PDA<Scope>
reputation_accountPDA<ReputationAccount>
sbt_token_accountToken-2022 Account
clockSysvar
NoteAll five scope slots are always passed. Slots that are empty, expired, or revoked are skipped internally; there is no difference in gas between a writ with one scope and one with five. The account list cost is constant.

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.

caller siderust
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

CodeWhen
WritNotFoundagent has no WritAccount
SbtMissingSBT token account is closed or has 0 balance
NoActiveDelegationall 5 scopes are revoked, expired, or empty
ProgramNotAllowedcaller's program_id is not in Scope.programs
BudgetExceededverify_and_record would exceed budget
ActionNotPermittedaction bit unset in Scope.actions
ClockSkewScope.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.