mountTrustGate
Drop-in Express middleware. Adds the four x402 routes to any app — verify, receipt, settle, dispute — with the atomicity guard enforced at compile-time and runtime.
mountTrustGate(app, config) adds four endpoints to any Express app. The atomicity guard runs at the top of the call so a missing or false atomicityEnforced flag fails synchronously before any route binds.
Source: trustgate/sdk/src/express.ts.
Signature
import { mountTrustGate } from "@agenttrust-sdk/trustgate/express";
export function mountTrustGate(
app: Application,
config: MountTrustGateConfig,
): Promise<void>;MountTrustGateConfig
interface MountTrustGateConfig extends AtomicityEnforced {
rpcUrl: string; // Solana RPC URL
facilitatorKeypair: Keypair; // signs emit_feedback CPIs + tx fees
defaultPolicyId?: number; // fallback if /verify body omits policyId
programIds?: ProgramIds; // defaults to DEFAULT_DEVNET_PROGRAM_IDS
network?: string; // x402 header label, e.g. "solana-devnet"
atomicityEnforced: true; // literal `true` — TS compile error on `false`
}The promise resolves once IDLs are loaded from the cluster and routes are bound. If the cluster doesn't have published IDLs, mountTrustGate rejects — call anchor idl init once per program before mounting.
Routes
POST /verify
Read-only gate_payment simulation. Same semantics as gatePayment().
POST /verify
Content-Type: application/json
{
"payerAgentAsset": "<base58 pubkey>",
"payeeAgentAsset": "<base58 pubkey>",
"amount": "1000000",
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"policyId": 1
}Responses:
HTTP/1.1 200 OK
X-Agent-Trust-Decision: Allow
X-Payment-Network: solana-devnetHTTP/1.1 402 Payment Required
X-Agent-Trust-Decision: Deny
X-Payment-Required: denied
X-Payment-Reason-Code: 6
X-Payment-Reason-Name: CounterpartyTierBelowMin
X-Payment-Network: solana-devnetHTTP/1.1 402 Payment Required
X-Agent-Trust-Decision: RequireValidation
X-Payment-Required: validation
X-Capability-Required: 366c075140aa69746625d4b733b55e267fc5c28387fd6d1c24901976ee3ddc42
X-Payment-Network: solana-devnetHeaders built via buildHeadersForDecision in trustgate/sdk/src/x402.ts.
GET /receipt/:paymentIdHashHex
FeedbackEmissionLog PDA lookup. Returns { exists: false } until the payment settles.
GET /receipt/6984738594e493bfd4314866840427a11e8e53677bc0ff4b98ae8aa39ce0c859
HTTP/1.1 200 OK
Content-Type: application/json
{
"exists": true,
"score": 100,
"isDispute": false,
"emittedAtSlot": 460466788,
"feedbackEmissionLog": "HB4BBi9jaD3VPcZkQQaH3DxukSqBiXfW8RejtaLa8bF3"
}The paymentIdHashHex parameter is the lowercase hex encoding of SHA-256(payment_id_string). The SDK exports deriveFeedbackLogPda(programId, paymentIdHashBytes) for callers that derive the PDA themselves.
POST /settle
Atomic settle path. The handler accepts the verify-time payment context plus the tag1/tag2/endpoint/feedback_uri metadata, builds the three-instruction transaction via composeAtomicSettleTx, and submits.
POST /settle
Content-Type: application/json
{
"payerAgentAsset": "<base58>",
"payeeAgentAsset": "<base58>",
"amount": "1000000",
"mint": "<base58>",
"policyId": 1,
"paymentIdHash": "6984738594e493bfd4314866840427a11e8e53677bc0ff4b98ae8aa39ce0c859",
"feedbackUri": "ipfs://…",
"score": 100,
"tag1": "successful",
"tag2": "x402",
"endpoint": "/protected"
}The route requires payer keypair access (the SPL transfer authority). The default Express handler reads it from req.body.payerKey for testing; production deploys typically wrap this route with their own auth + signature scheme. See Pay.sh adapter for the canonical wrapping pattern.
POST /dispute
Same shape as /settle, but invokes dispute_payment (negative-score feedback) instead. Requires dispute_reason_hash in the body. The same FeedbackEmissionLog idempotency rules apply: a second dispute for the same payment_id_hash fails account-already-in-use.
Atomicity guard
mountTrustGate is itself a MountTrustGateConfig extends AtomicityEnforced consumer:
export interface MountTrustGateConfig extends AtomicityEnforced {
// … the literal `atomicityEnforced: true` is required
}export async function mountTrustGate(app, config) {
assertAtomicityEnforced(config, "mountTrustGate");
// …
}Compile-time: await mountTrustGate(app, { …, atomicityEnforced: false }); is a TS error. Runtime: await mountTrustGate(app, { …, atomicityEnforced: true as any }); works because TS would have already let it through, BUT await mountTrustGate(app, JSON.parse(unsafeUserConfig)); still fails the runtime guard.
Six SDK unit tests cover both layers in trustgate/sdk/test/atomicity.test.ts — including the as any cast bypass and the composeAtomicSettleTx three-instruction structure check.
Example
import express from "express";
import { Keypair } from "@solana/web3.js";
import { mountTrustGate } from "@agenttrust-sdk/trustgate/express";
const app = express();
app.use(express.json());
const facilitatorKeypair = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(process.env.FACILITATOR_KEYPAIR!)),
);
await mountTrustGate(app, {
rpcUrl: process.env.SOLANA_RPC_URL ?? "https://api.devnet.solana.com",
facilitatorKeypair,
defaultPolicyId: 1,
network: "solana-devnet",
atomicityEnforced: true,
});
app.get("/", (_req, res) => res.send("AgentTrust facilitator up"));
app.listen(3000, () => console.log("listening on :3000"));The hosted facilitator at api.agenttrust.tech runs exactly this code path. Health: GET /healthz.
Source
- Express middleware:
trustgate/sdk/src/express.ts - x402 helpers:
trustgate/sdk/src/x402.ts - Atomicity tests:
trustgate/sdk/test/atomicity.test.ts