Protocol

Masque the disguise

Identity & attestation

A Masque is the on-chain credential that binds an agent to a principal. It is a PDA, so its existence and state are universally verifiable — any merchant, relayer, or peer agent can check whether a payer's Masque is active without calling any server.

Account shape

programs/masque/src/lib.rs
#[account]
pub struct Masque {
    pub principal: Pubkey,          // issuer — 32
    pub agent: Pubkey,              // holder — 32
    pub scope_hash: [u8; 32],       // off-chain scope policy digest
    pub issued_at: i64,
    pub expires_at: i64,
    pub revoked_at: i64,            // 0 = active
    pub revocation_reason: u8,
    pub action_count: u64,
    pub reputation: u32,            // bounded 0..=10_000
    pub last_action_at: i64,
    pub last_action_hash: [u8; 32],
}

Lifecycle

Every Masque moves through this graph. The revoked state is terminal — there's no un-revoke. Principals issue a new Masque to rotate keys.

txt
          issue
   [ ∅ ] ───────► [ active ]
                      │
             ┌────────┼─────────┐
             │        │         │
           attest   rotate    revoke
           (N×)    scope      (terminal)
             │        │         │
             └────────┘         ▼
                            [ revoked ]

Instructions

issue

Principal signs. Creates the PDA, sets expiration, initializes reputation to 100.

ts
await masque.methods
  .issue(agentPubkey, scopeHash, lifespanSeconds)
  .accounts({ masque: masquePda, principal, systemProgram })
  .rpc();

attest

Agent signs. Records a 32-byte action digest and bumps reputation. Fails if the Masque is revoked or expired. The hash is arbitrary: typically the hash of the x402 descriptor that was settled, so the attestation graph indexes directly to real payment events.

ts
await masque.methods
  .attest(actionHash)
  .accounts({ masque: masquePda, agent })
  .signers([agentKeypair])
  .rpc();

revoke

Principal-only. Sets revoked_at = now. From the next slot, every downstream program checks this field and refuses. Reason codes are open-vocabulary (suggested: 1=rotation, 2=compromise, 3=misbehavior, 4=lifecycle end).

ts
await masque.methods
  .revoke(2) // reason: compromise
  .accounts({ masque: masquePda, principal })
  .rpc();
Warning
Revocation is permanent. If the agent is ever repaired and put back to work, issue a fresh Masque under a new agent keypair. This is deliberate — reusing a compromised identity defeats the point.

Reputation

Reputation starts at 100 and gains +1 per attested action, capped at 10,000. It's intentionally simple at the protocol level. Sophisticated reputation markets (weighted by merchant category, settlement size, peer attestations) run off-chain on top of the Masque attestation stream — the chain stores the raw integrity signal and lets builders layer what they need.

Events emitted

rust
#[event] pub struct MasqueIssued   { principal, agent, expires_at }
#[event] pub struct ActionAttested { agent, action_hash, reputation }
#[event] pub struct MasqueRevoked  { principal, agent, reason, at }

Helius webhooks or any geyser plugin can surface these in real time to feed downstream reputation or fraud systems.