Start

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

high-level
                 ┌──────────────────────────┐
 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.

Note
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:

programs/ombre/src/lib.rs
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

AccountSeedsOwner
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
Tip
The separation between Masque and Serrure is deliberate. An agent can have many Serrures (one per venue/use case) all bound to the same Masque. Revoking the Masque instantly invalidates all of them.