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 emissionThe 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>;
}| Method | Why 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
| Facilitator | Repo status | Meaning |
|---|---|---|
| Pay.sh | concrete adapter | Ready for the demo + production dependency factory; the canonical FacilitatorAdapter reference |
| Dexter | in-flight stub | Worked example for proving adapter portability |
| atxp | roadmap stub | Tracked, not silently unsupported |
| MCPay | roadmap stub | Tracked, not silently unsupported |
agenttrust_list_facilitators (an MCP tool) returns the live status set: MCP → Tools.
Add a facilitator
- Read the facilitator's wire format. Identify challenge headers, proof headers, body shape, recipient encoding, payment ID hints, Allow/Deny responses.
- Define strict Zod schemas for the request body and proof payload. Reject unknown root fields.
- Implement one
FacilitatorAdapterclass. - Re-export from
trustgate/server/src/facilitators/index.ts. - Register with
FacilitatorRegistry. - Add unit tests mirroring Pay.sh coverage (50+ cases — schema, signature, proof-validator, feedback idempotency).
- Add a demo under
examples/<name>-demoif 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
paymentIdHashmatches 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 —
priorEmissionLookupshort-circuits a second emission for the samepayment_id_hash - stable reason codes on
Deny— clients consume the numericreasonCode, 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:
parseRequestaccepts the verify-shape and rejects malformed inputs with HTTP400.formatChallengeemits the right headers per decision arm (Allow, Deny + reason, RequireValidation + capability hash).validatePaymentProofrejects every entry in the hostile-input matrix.emitFeedbackis idempotent — a second call for the samepayment_id_hashreturns 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.