Solana ProgramsMandateRegistry

MandateRegistry anchors the existence, agent binding and revocation of every spending mandate. It deliberately does not store the limits: an owner’s spending caps are confidential, and the protocol proves them another way (below).

Program ID (devnet)8HAzw3UFGmabsHJkAsuGLfBZG8djYQ3J1FRNUVjkseMr
FrameworkAnchor 0.31.1
Upgradeable?On devnet, yes — the upgrade authority 47BBdKL1SwY7auTN4mQpnQAFX14bCjs7qie9R34FrXxc is held by Regent so fixes can ship during the pilot phase. ADR-010 commits the core programs to be non-upgradeable on mainnet
Sourceregent-protocol/regent-solana-programs → programs/mandate-registry

Account layout

#[account]
pub struct Mandate {
    pub mandate_id: [u8; 32],    // the mandate UUID's 16 bytes, left-padded with zeros
    pub agent_id: [u8; 32],      // the agent's PDA seed in AgentRegistry: sha256(agent id)
    pub registered_at: i64,      // unix seconds
    pub revoked: bool,
    pub revoked_at: Option<i64>,
    pub bump: u8,
}
// 8-byte discriminator + 83 bytes

PDA seeds: [b"mandate", mandate_id]. agent_id is the same 32 bytes AgentRegistry uses as the agent’s PDA seed, so a mandate resolves to its agent account; mandates registered before 2026-10-04 carry a differently derived reference (the id was zero-padded before hashing) and do not resolve.

No per_tx_limit, daily_limit, monthly_limit, currency or status fields. Earlier versions of this page described such fields; the deployed program never had them. The only on-chain transition is revoked: false → true; suspended (agent revoked) is an off-chain state.

Why the limits are not on-chain

A public ledger that carried every owner’s ceilings would publish their spending policy to the world. The program therefore anchors presence (this mandate id exists, bound to this agent, since this time) and revocation, and nothing else.

The limits are proven through the decision evidence instead:

  • Every change to the terms creates an immutable version with a salted SHA-256 commitment (mandate_version, mandate_hash).
  • Every allow token, every completion receipt and every audit event carries that version and hash. The audit events are Merkle-batched and anchored by AuditAnchor.
  • The owner reveals a version — snapshot plus salt — with GET /v1/organizations/{org}/mandates/{id}/versions/{version}; the verifier recomputes the hash and matches it to the receipt.

An auditor reviewing a disputed MANDATE_LIMIT_EXCEEDED refusal therefore reads the limit from the revealed version named in the receipt, not from the chain — and the chain guarantees the audit event that names that version was not rewritten.

On the optional EVM anchor (Arbitrum Nova), the registry stores the terms commitment (the hash) alongside the ids — still never the amounts.

Instructions

register_mandate

Writes a new Mandate account with revoked = false.

AccountTypeNotes
configPDA [b"config"]Holds the authority and the mandate counter
mandatePDA [b"mandate", mandate_id]Initialized; rent paid by the authority
authoritySignerMust match config.authority
system_programProgram—
ArgType
mandate_id[u8; 32]
agent_id[u8; 32]

Off-chain, a mandate created through the API is active immediately; blockchain-worker then registers it here asynchronously and records the transaction signature on the mandate. Authorizations do not wait for the transaction.

revoke_mandate

Sets revoked = true and stamps revoked_at. Reverts if already revoked. Called by blockchain-worker after POST /v1/organizations/{org}/mandates/{id}/revoke.

transfer_authority

Moves config.authority to a new key (used for the 2026-09-26 rotation).

Verifying a mandate off-chain

const uuidToBytes32 = (u: string) =>
  Buffer.concat([Buffer.alloc(16), Buffer.from(u.replace(/-/g, ""), "hex")]);
 
const [mandatePda] = PublicKey.findProgramAddressSync(
  [Buffer.from("mandate"), uuidToBytes32(mandateId)],
  programId,
);
const account = await program.account.mandate.fetch(mandatePda);
 
console.log("Registered:", new Date(account.registeredAt * 1000));
console.log("Revoked:", account.revoked, account.revokedAt);

If the account is missing, the mandate was never registered on-chain. If revoked is true, no authorization issued after revoked_at should be trusted, whatever the off-chain API says. For the limits, verify the receipt — see Verifying on-chain.