AgentTrust
AgentTrust
Integration guides

Facilitator adapters

Add a new x402 facilitator without rewriting AgentTrust routes or policy. Five-method contract, registry-based dispatch, conformance-tested.

The adapter layer is what makes AgentTrust facilitator-agnostic. Pay.sh is the canonical reference; Dexter is the in-flight second worked example; atxp and MCPay are roadmap. Routes (/verify, /settle, /dispute), the PolicyVault gate, and the registry reads do not branch on facilitator name. Protocol quirks live in one adapter file.

The adapter implementations live in trustgate/server/src/facilitators/. The server package is workspace-internal (not published to npm); copy the pattern from facilitators/pay-sh/ into your own server, or use the published SDK middleware which dispatches through the adapter layer for you.

Mental model

HTTP request
   → /verify · /settle · /dispute
   → FacilitatorRegistry  (selects adapter by X-Facilitator header or default)
   → active FacilitatorAdapter
   → VerifyContext
   → PolicyVault gate decision
   → settlement proof validation
   → feedback emission

The active adapter is the only code that knows about the chosen facilitator.

The five-method contract

export interface FacilitatorAdapter {
  readonly protocol: FacilitatorProtocol; // identity tag for registry

  parseRequest(req: Request): Promise<VerifyContext>;
  formatChallenge(decision: GateDecision, ctx: VerifyContext): ChallengeResponse;
  formatSettlement(ctx: VerifyContext): SettlementResponse;
  validatePaymentProof(proof: unknown, ctx: VerifyContext): Promise<PaymentProofValidation>;
  emitFeedback(ctx: VerifyContext, settlement: ConfirmedSettlement): Promise<FeedbackEmissionResult>;
}
MethodWhy it exists
parseRequest(req)Translate the facilitator's request body and headers into VerifyContext
formatChallenge(decision, ctx)Render Allow / Deny / RequireValidation in that facilitator's wire format
formatSettlement(ctx)Return protocol-specific settlement metadata or an unsigned transaction skeleton
validatePaymentProof(proof, ctx)Verify proof shape and cross-check against the verify-time context
emitFeedback(ctx, settlement)Emit the AgentTrust feedback record idempotently

If an adapter needs a sixth method, that is a contract change, not a one-off escape hatch.

Status map

FacilitatorRepo statusMeaning
Pay.shconcrete adapterReady for the demo + production dependency factory; the canonical FacilitatorAdapter reference
Dexterin-flight stubWorked example for proving adapter portability
atxproadmap stubTracked, not silently unsupported
MCPayroadmap stubTracked, not silently unsupported

agenttrust_list_facilitators (an MCP tool) returns the live status set: MCP → Tools.

Add a facilitator

  1. Read the facilitator's wire format. Identify challenge headers, proof headers, body shape, recipient encoding, payment ID hints, Allow/Deny responses.
  2. Define strict Zod schemas for the request body and proof payload. Reject unknown root fields.
  3. Implement one FacilitatorAdapter class.
  4. Re-export from trustgate/server/src/facilitators/index.ts.
  5. Register with FacilitatorRegistry.
  6. Add unit tests mirroring Pay.sh coverage (50+ cases — schema, signature, proof-validator, feedback idempotency).
  7. Add a demo under examples/<name>-demo if the integration needs a runnable walkthrough.

Registry wiring

import { FacilitatorRegistry, PaySh } from "./facilitators";
import { Dexter } from "./facilitators/dexter";

const registry = new FacilitatorRegistry();
registry.register(new PaySh(payShDeps));
registry.register(new Dexter(dexterDeps));
registry.setDefault("pay-sh");

// Per-request selection via X-Facilitator header:
//   curl -H "X-Facilitator: dexter" …
// Falls back to default if header is absent.

The registry resolves the active adapter on every /verify, /settle, /dispute call. The dispatch site is one line; nothing in the route layer knows which adapter is active.

Security checklist

Every adapter must preserve the same proof-of-payment checks:

  • replay defense — reject proofs whose paymentIdHash matches a prior settlement
  • self-pay defense — reject proofs where transfer authority equals facilitator fee payer
  • amount, mint, recipient cross-checks — reject if the proof references different values than the verify-time context
  • network match — reject mainnet proofs against a devnet-bound facilitator
  • expiry window — reject proofs older than the SERVICE-signed challenge's issuedAt + maxTimeoutSeconds
  • idempotent feedback emission — priorEmissionLookup short-circuits a second emission for the same payment_id_hash
  • stable reason codes on Deny — clients consume the numeric reasonCode, decoupled from Borsh wire-format ordering

The Pay.sh adapter's proof-validator.ts and feedback.ts are the reference pattern.

Conformance test

CI runs .github/workflows/adapter-contract-conformance.yml on every PR. The workflow walks every registered adapter and asserts:

  • parseRequest accepts the verify-shape and rejects malformed inputs with HTTP 400.
  • formatChallenge emits the right headers per decision arm (Allow, Deny + reason, RequireValidation + capability hash).
  • validatePaymentProof rejects every entry in the hostile-input matrix.
  • emitFeedback is idempotent — a second call for the same payment_id_hash returns the prior emission's signature instead of double-emitting.

Adversarial-harness coverage: Verification → Adversarial harness.

Target integration time

The architecture target is under two hours for a facilitator that already has a clear x402 wire spec. If adding a facilitator forces route edits, policy edits, or registry-read changes, the adapter boundary failed and gets fixed before the integration is considered done.

On this page

⌘I