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 |
| Framework | Anchor 0.30 |
| Upgradeable? | No |
| Source | packages/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_EXCEEDEDrejection 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.
| Arg | Type |
|---|---|
mandate_id | [u8; 16] |
agent_id_hash | [u8; 32] |
per_tx_limit | Option<u64> |
daily_limit | Option<u64> |
monthly_limit | Option<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-chain | On-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.