Architecture
Arsene is three Anchor programs plus an SDK. The programs are independent accounts — you can deploy Masque without Ombre — but they compose cleanly. The defining move is the CPI from Ombre into Serrure: every payment is policy-checked in the same transaction that moves the funds. No hook, no callback, no race.
Data flow
┌──────────────────────────┐
PRINCIPAL ────► │ MASQUE Registry │ ◄──── Revocation
(human / │ (PDA per agent) │ Reputation
DAO / └────────────┬─────────────┘
enterprise) │ credential
▼
┌──────────────────────────┐
│ AI AGENT │
│ (ElizaOS / LangGraph / │
│ Vercel AI SDK / ...) │
└────────────┬─────────────┘
│ @arsene/core
┌──────────┼──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ SERRURE │ │ OMBRE │ │ x402 │
│ (policy) │◄─┤ (private │──│ merchant │
│ │ │ pool) │ │ │
└──────────┘ └──────────┘ └──────────┘
▲ CPI from Ombre::settle before any lamports move.
If Serrure rejects, the whole tx unwinds.Programs
masque
Identity and attestation registry. One PDA per (principal, agent) pair at seeds [b"masque", principal, agent_pubkey]. Stores the credential, expiration, revocation state, action count, and reputation. All instructions: issue, attest, revoke, rotate_scope.
serrure
On-chain policy engine. One PDA per (principal, agent) pair at seeds [b"serrure", principal, agent]. Enforces per-tx max, sliding-window rate limit, daily cap, lifetime cap, merchant merkle-root allowlist, and principal-controlled circuit breaker. Instructions: create, update, pause, unpause, check.
check is the hot path. Ombre CPIs into it before settlement. The check and the transfer live in the same Solana transaction — there is no window in which a bad payment can escape.ombre
Private payment pool. Agents deposit under stealth commitments (H(viewing_key || amount || nonce)). Settlement releases funds to a merchant only after serrure::check clears. The reference implementation uses simple commitments; production uses Light Protocol compressed accounts and ZK proofs of valid decommitment. Instructions: init_pool, deposit, settle.
CPI composition
The core Ombre → Serrure CPI looks like this on-chain:
pub fn settle<'info>(
ctx: Context<'_, '_, '_, 'info, Settle<'info>>,
amount_lamports: u64,
merchant: Pubkey,
x402_descriptor_hash: [u8; 32],
serrure_merkle_proof: Vec<[u8; 32]>,
) -> Result<()> {
// --- CPI into Serrure BEFORE any lamports move. --- //
let cpi_program = ctx.accounts.serrure_program.to_account_info();
let cpi_accounts = serrure::cpi::accounts::CheckPolicy {
serrure: ctx.accounts.serrure.to_account_info(),
agent: ctx.accounts.agent.to_account_info(),
};
serrure::cpi::check(
CpiContext::new(cpi_program, cpi_accounts),
amount_lamports, merchant, serrure_merkle_proof,
)?;
// Serrure cleared — safe to move money. Any failure above
// unwinds the whole transaction, vault balance unchanged.
**ctx.accounts.vault.to_account_info().try_borrow_mut_lamports()? -= amount_lamports;
**ctx.accounts.merchant_account.try_borrow_mut_lamports()? += amount_lamports;
// ...
}Account layout summary
| Account | Seeds | Owner |
|---|---|---|
| Masque | ["masque", principal, agent] | principal |
| Serrure | ["serrure", principal, agent] | principal |
| Pool | ["ombre", "pool"] | protocol DAO |
| Vault | ["ombre", "vault"] | pool PDA |
| Note | ["note", commitment] | agent / depositor |