AgentTrust
AgentTrust
architecture

Architecture

How the three Anchor programs, the SDK, and the facilitator-adapter layer compose into one settlement path.

AgentTrust is three Anchor programs plus a TypeScript surface that mounts them on any x402 facilitator. This page is the wiring. Byte-precise PDA layouts, instruction signatures, and deny-reason codes live under Programs.

Composition diagram

┌───────────────────────────────────────────────────────────────────────┐
│  x402 Facilitator (Pay.sh ★ default · Dexter · atxp · MCPay)          │
│                                                                       │
│   import { mountTrustGate } from "@agenttrust-sdk/trustgate/express"  │
│   await mountTrustGate(app, { atomicityEnforced: true, … })           │
│                                                                       │
│  ★ Pay.sh = Solana Foundation's first x402 facilitator (May 5, 2026)  │
└───────────────────────────────────────────────────────────────────────┘

                              ▼  POST /verify | /settle | /dispute
       ┌──────────────────────────────────────────────────────────┐
       │  TrustGate (Anchor program)                              │
       │  ├─ init_authority (per-facilitator PDA)                 │
       │  ├─ emit_feedback  ── PDA-signed CPI ──┐                 │
       │  └─ dispute_payment                    │                 │
       └─────────────────┬──────────────────────┼─────────────────┘
                         │                      │
                         │ CPI                  │ CPI
                         ▼                      ▼
       ┌─────────────────────────────┐  ┌──────────────────────────────┐
       │  PolicyVault                │  │  Quantu agent-registry-8004  │
       │  ├─ gate_payment composer   │  │  ├─ give_feedback            │
       │  │   (fail-fast 5 policies) │  │  └─ atom-engine::AtomStats   │
       │  ├─ KillSwitch • Spending   │  │     (tier @ byte 551)        │
       │  ├─ Velocity • Counterparty │  │                              │
       │  └─ RequireValidation       │  │  Pinned commit: bfb09ad      │
       │                             │  │  Read via byte-offset parser │
       │  reads ValidationAttestation│  │     (zero Cargo dep)         │
       └─────────────────┬───────────┘  └──────────────────────────────┘


       ┌──────────────────────────────────────────┐
       │  ValidationRegistry                      │
       │  ├─ register_namespace / _attestor       │
       │  ├─ request / respond / revoke           │
       │  └─ ValidationAttestation PDA            │
       │     (read by PolicyVault parser)         │
       └──────────────────────────────────────────┘

A single composeAtomicSettleTx() call in the SDK builds one Solana transaction with three instructions in canonical order: gate_payment_strict → SPL transferCheckedemit_feedback. The transaction either commits all three or none — never two of three. That is the load-bearing safety property the SDK enforces at three layers.

The three programs

PolicyVault — the decision engine

Five orthogonal policy kinds compose under one gate_payment instruction with fail-fast semantics:

OrderPolicyWhat it checks
1KillSwitchper-agent paused flag — multisig-controlled emergency stop
2Spendingper-tx, daily (UTC midnight), weekly (ISO Monday) limits
3Velocitysliding-window cumulative spend, payer-tier-decayed limits
4CounterpartyTierpayee AtomStats.trust_tier (byte 551), risk score, confidence
5RequireValidationgates against a ValidationAttestation PDA (capability proof)

The composer returns Allow, Deny(reason), or RequireValidation(capability_hash). State changes — daily / weekly counters, velocity ledger — apply only on Allow. Deny and RequireValidation mutate nothing.

A strict variant — gate_payment_strict — converts non-Allow into Err. That is the variant the SDK's atomic composer uses: any Deny on the gate fails the entire bundled transaction. The Phase J5 Kani proof gate_payment_strict_correctness pins the biconditional — strict returns Ok(()) if and only if the lazy composer returns Allow — so a future change cannot silently re-route the Deny arm to an Ok return.

Byte-precise reference: PolicyVault. Composer: Composer.

TrustGate — the facilitator-side program

Two PDAs, three instructions:

PDASeedsRole
TrustGateAuthority["trustgate_auth", facilitator]Per-facilitator PDA signer for the CPI
FeedbackEmissionLog["feedback_log", payment_id_hash]Init-only idempotency receipt

emit_feedback PDA-signs a CPI into Quantu's agent-registry-8004::give_feedback. The FeedbackEmissionLog is created via Anchor's init constraint — a second tx with the same payment_id_hash fails account-already-in-use, so retries cannot double-emit. dispute_payment writes a negative-score variant for disputed payments.

Byte-precise reference: TrustGate. SDK reference: SDK → mountTrustGate.

ValidationRegistry — the third ERC-8004 leg

Five instructions over four PDAs:

InstructionEffect
register_namespacepermissionless; caller computes SHA256(name_utf8)
register_attestorself-registration with display URI
request_validationopen a request for (subject, capability)
respond_to_validationattestor writes the attestation PDA
revoke_validationoriginal attestor sets revoked = true

The v1 sybil-resistance model is downstream-consumer filtering: PolicyVault stores a per-policy accepted_attestors[] array (length 2). Only attestations from those keys flip RequireValidation to Allow. Permissionless registration plus opinionated downstream filtering trades global gatekeeping for local trust — the only model that scales with the number of facilitators.

Byte-precise reference: ValidationRegistry. v1 capability namespaces seeded on devnet: Reference → Capability namespaces.

The FacilitatorAdapter pattern

Every x402 facilitator carries the same payment intent but differs in headers, body shape, proof payload, retry behavior, and settlement metadata. The adapter layer isolates those differences in five methods:

MethodResponsibility
parseRequest(req)translate the facilitator's request into VerifyContext
formatChallenge(decision, ctx)render Allow / Deny / RequireValidation in that facilitator's wire format
formatSettlement(ctx)produce settlement metadata or an unsigned transaction skeleton
validatePaymentProof(proof, ctx)verify proof shape and cross-check the verify-time context
emitFeedback(ctx, settlement)call the feedback CPI idempotently

Routes (/verify, /settle, /dispute), the policy gate, and the registry reads do not branch on facilitator name. Pay.sh is the canonical implementation in trustgate/server/src/facilitators/pay-sh/; Dexter, atxp, and MCPay share the same interface. Adding a fifth facilitator stays under a hundred lines.

Walkthrough: Pay.sh adapter. Build your own: Facilitator adapters.

The atomic-tx invariant

gate_payment_strict mutates state on Allow:

  • PolicyAccount.spending_today_used, spending_today_anchor
  • PolicyAccount.spending_week_used, spending_week_anchor
  • VelocityLedger.cumulative_amount, last_commit_slot

If the SPL transfer reverts after that mutation — and the two instructions are in different Solana transactions — the gate's state is durable while no value moved. Subsequent gate_payment calls read the inflated counters and may deny a legitimate payment, or velocity caps appear hit when nothing real has happened.

The corruption vector is most cleanly triggered by a Token-2022 mint with a TransferHook extension that reverts on a compliance check, but it is mint-extension-agnostic: any failing inner instruction in a split flow corrupts state the same way.

The SDK closes the corruption vector at three layers:

  1. Compile-time literal-type guard. AtomicityEnforced is the literal type { atomicityEnforced: true }. TypeScript rejects callers passing false or omitting the field. No runtime cost.
  2. Runtime guard. assertAtomicityEnforced throws AtomicityNotEnforcedError for any value that isn't strictly === true. Catches as any cast bypasses.
  3. Composer structure. composeAtomicSettleTx returns one Transaction with exactly three instructions in canonical order (gate, transfer, feedback). Any other shape is a programming error.

Plus the on-chain Kani proof gate_payment_strict_correctness pins the strict handler's contract end-to-end: Ok(()) if and only if the composer returned Allow. Full proof: Verification → Atomic-tx invariant. Background: docs/proofs/transfer-hook-atomicity.md.

ERC-8004 wiring

Quantu Labs published two of the three ERC-8004 legs on Solana:

  • agent-registry-8004 — agent identity plus the reputation surface (the give_feedback instruction). Each agent gets a Metaplex Core asset plus a Borsh-typed AgentAccount PDA.
  • atom-engine — confidence-weighted reputation aggregation. The AtomStats PDA stores tier_immediate and tier_confirmed as single bytes.

The third leg — capability validation — was scaffolded in 8004-solana v0.4 then archived in v0.5.0 pending a redesign. AgentTrust's ValidationRegistry productizes it. The semantics align with the ERC-8004 V variant: who attests to a capability the payer's policy requires (KYC tier, audit attestation, jurisdictional compliance, model-card provenance, payment-network membership).

PolicyVault reads from both Quantu legs:

FieldSource PDAOffsetWidth
trust tier (immediate)AtomStatsbyte 551u8
trust tier (confirmed)AtomStatsbyte 555u8
risk scoreAtomStatsbyte 549u8
confidenceAtomStatsbyte 557u16 LE
schema version canaryAtomStatsbyte 560u8 (must equal 1)

A schema bump on the Quantu side fails the canary check loud rather than silently misreading fields. Full byte-offset reference: Reference → Byte offsets.

On this page

⌘I