Solana ProgramsMandateRegistry

MandateRegistry anchors the existence, limits, and status of every spending mandate. A regulator examining a disputed authorization can verify the exact limits that were in effect at the time of the authorization.

Program ID (devnet)8HAzw3UFGmabsHJkAsuGLfBZG8djYQ3J1FRNUVjkseMr
FrameworkAnchor 0.30
Upgradeable?No
Sourcepackages/contracts-solana/programs/mandate-registry

Account layout

#[account]
pub struct Mandate {
    pub mandate_id: [u8; 16],         // raw UUID bytes
    pub agent_id_hash: [u8; 32],
    pub per_tx_limit: Option<u64>,    // base units of the configured currency
    pub daily_limit: Option<u64>,
    pub monthly_limit: Option<u64>,
    pub currency: [u8; 8],            // null-terminated ASCII, e.g. "USD"
    pub status: MandateStatus,        // Pending | Active | Suspended | Revoked
    pub created_at: i64,
    pub activated_at: Option<i64>,
    pub bump: u8,
}

PDA seeds: [b"mandate", mandate_id].

Why limits are on-chain

The dollar amounts in per_tx_limit, daily_limit, and monthly_limit are stored directly on Solana, not just hashed. This is intentional:

  • An auditor reviewing a disputed MANDATE_LIMIT_EXCEEDED rejection can confirm the exact limit value at the time
  • A regulator can query the on-chain history of limit changes (each revoke + re-create creates new PDAs)
  • The responsible party cannot retroactively claim the limit was different

The amounts are stored as base units of the configured currency — for USD, that’s cents. The off-chain API serializes them as decimal strings for ergonomics, but the on-chain representation is integer-safe.

Instructions

register_mandate

Writes a new Mandate account in Active state.

ArgType
mandate_id[u8; 16]
agent_id_hash[u8; 32]
per_tx_limitOption<u64>
daily_limitOption<u64>
monthly_limitOption<u64>
currency[u8; 8]

Off-chain, api-payment first inserts the mandate with status pending. The blockchain-worker then calls register_mandate. On confirmation, an onchain.mandate_registered event flips the off-chain status to active.

revoke_mandate

Sets status to Revoked. Distinct from Suspended — the latter is a soft state triggered by agent revocation, while Revoked is an explicit off-switch by the responsible party.

The on-chain program does not distinguish Suspended from Active (those are off-chain concerns). Only Revoked is an explicit on-chain transition.

Currency representation

The currency field is fixed-width 8 bytes, null-terminated ASCII. Examples:

Off-chainOn-chain bytes
"USD"0x55 0x53 0x44 0x00 0x00 0x00 0x00 0x00
"EUR"0x45 0x55 0x52 0x00 0x00 0x00 0x00 0x00
"USDC"0x55 0x53 0x44 0x43 0x00 0x00 0x00 0x00

This keeps the account a fixed size while still supporting major fiat and stablecoin denominations.

Verifying a mandate off-chain

const [mandatePda] = PublicKey.findProgramAddressSync(
  [Buffer.from("mandate"), Buffer.from(mandateId, "hex")],
  programId,
);
const account = await program.account.mandate.fetch(mandatePda);
 
console.log("Per-tx:", account.perTxLimit?.toString() ?? "∞");
console.log("Daily:", account.dailyLimit?.toString() ?? "∞");
console.log("Status:", account.status);

If the on-chain limits differ from what the API reports, trust the chain — that’s the regulatory-safe choice.