AgentTrust
AgentTrust
SDK

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-devnet
HTTP/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-devnet
HTTP/1.1 402 Payment Required
X-Agent-Trust-Decision: RequireValidation
X-Payment-Required: validation
X-Capability-Required: 366c075140aa69746625d4b733b55e267fc5c28387fd6d1c24901976ee3ddc42
X-Payment-Network: solana-devnet

Headers 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

On this page

⌘I