@agenttrust-sdk/mcp 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/embedded-data/devnet-attestor-trace.json +32 -0
  2. package/dist/embedded-data/devnet-chained-validation.json +52 -0
  3. package/dist/embedded-data/devnet-counterparties.json +53 -0
  4. package/dist/embedded-data/devnet-demo-policies.json +46 -0
  5. package/dist/embedded-data/devnet-namespaces.json +107 -0
  6. package/dist/embedded-data/devnet-smoke.json +24 -0
  7. package/dist/embedded-docs/getting-started/architecture-overview.mdx +85 -0
  8. package/dist/embedded-docs/getting-started/quickstart.mdx +100 -0
  9. package/dist/embedded-docs/index.mdx +64 -0
  10. package/dist/embedded-docs/integration-guides/capability-namespaces.mdx +15 -0
  11. package/dist/embedded-docs/integration-guides/custom-attestor.mdx +15 -0
  12. package/dist/embedded-docs/integration-guides/facilitator-adapters.mdx +85 -0
  13. package/dist/embedded-docs/integration-guides/pay-sh-adapter.mdx +110 -0
  14. package/dist/embedded-docs/integration-guides/x402-facilitator.mdx +79 -0
  15. package/dist/embedded-docs/programs/policy-vault/counterparty-tier-policy.mdx +15 -0
  16. package/dist/embedded-docs/programs/policy-vault/index.mdx +68 -0
  17. package/dist/embedded-docs/programs/policy-vault/kill-switch-policy.mdx +15 -0
  18. package/dist/embedded-docs/programs/policy-vault/require-validation-policy.mdx +15 -0
  19. package/dist/embedded-docs/programs/policy-vault/spending-policy.mdx +15 -0
  20. package/dist/embedded-docs/programs/policy-vault/velocity-policy.mdx +15 -0
  21. package/dist/embedded-docs/programs/trustgate.mdx +53 -0
  22. package/dist/embedded-docs/programs/validation-registry.mdx +49 -0
  23. package/dist/embedded-docs/reference/byte-offset-reference.mdx +20 -0
  24. package/dist/embedded-docs/reference/changelog.mdx +19 -0
  25. package/dist/embedded-docs/reference/devnet-program-ids.mdx +24 -0
  26. package/dist/embedded-docs/reference/discriminator-constants.mdx +16 -0
  27. package/dist/embedded-docs/reference/formal-verification.mdx +19 -0
  28. package/dist/embedded-docs/reference/mainnet-program-ids.mdx +16 -0
  29. package/dist/embedded-docs/reference/quantu-agent-registry.mdx +15 -0
  30. package/dist/embedded-docs/sdk/atomic-tx-invariant.mdx +37 -0
  31. package/dist/embedded-docs/sdk/gate-payment.mdx +22 -0
  32. package/dist/embedded-docs/sdk/index.mdx +73 -0
  33. package/dist/embedded-docs/sdk/mount-trustgate.mdx +15 -0
  34. package/dist/embedded-examples/attestor-demo/README.md +100 -0
  35. package/dist/embedded-examples/pay-sh-demo/README.md +136 -0
  36. package/dist/embedded-examples/pay-sh-demo/src/deps-real.ts +150 -0
  37. package/dist/embedded-examples/pay-sh-demo/src/deps.ts +150 -0
  38. package/dist/embedded-examples/pay-sh-demo/src/index.ts +471 -0
  39. package/dist/embedded-examples/pay-sh-demo/src/middleware.ts +198 -0
  40. package/dist/embedded-examples/pay-sh-demo/src/policy.ts +247 -0
  41. package/dist/embedded-examples/pay-sh-demo/src/x402.ts +140 -0
  42. package/dist/index.js +73 -18
  43. package/dist/index.js.map +1 -1
  44. package/dist/resources/docs.js +68 -46
  45. package/dist/resources/docs.js.map +1 -1
  46. package/dist/server.js +6 -1
  47. package/dist/server.js.map +1 -1
  48. package/dist/tools/discovery/docs.js +6 -0
  49. package/dist/tools/discovery/docs.js.map +1 -1
  50. package/dist/tools/discovery/facilitator-walkthrough.js +43 -18
  51. package/dist/tools/discovery/facilitator-walkthrough.js.map +1 -1
  52. package/dist/tools/read/demo-state.js +7 -4
  53. package/dist/tools/read/demo-state.js.map +1 -1
  54. package/dist/tools/read/simulate-payment.js +13 -1
  55. package/dist/tools/read/simulate-payment.js.map +1 -1
  56. package/dist/trustgate/server/src/facilitators/README.md +241 -0
  57. package/package.json +2 -2
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Demo `PayShDeps` factory.
3
+ *
4
+ * The demo runs without a Solana RPC connection — `validateOnChainTx`
5
+ * synthesises a confirmed-tx fixture from `ctx`-implied fields, and
6
+ * `emitFeedbackCpi` returns a deterministic synthetic signature.
7
+ *
8
+ * This is intentional: the demo's purpose is to prove the AgentTrust
9
+ * pipeline (parse → policy gate → format → validate-shape → emit) end-to-end.
10
+ * Real proof-of-payment confirmation is an integration concern that lives
11
+ * in the production deps factory (a future module that wires Anchor +
12
+ * spl-token tx parsing). Demo flows the happy path.
13
+ */
14
+
15
+ import { PublicKey } from "@solana/web3.js";
16
+
17
+ import {
18
+ ConfirmedSettlement,
19
+ EmitFeedbackInput,
20
+ OnChainTxValidation,
21
+ PayShDeps,
22
+ PriorEmissionLookup,
23
+ ReplayCache,
24
+ ValidateOnChainTxFn,
25
+ VerifyContext,
26
+ } from "@agenttrust/trustgate-server";
27
+
28
+ const SYNTHETIC_SIG_PREFIX = "demo-sig-";
29
+ const SYNTHETIC_FEEDBACK_PREFIX = "demo-feedback-";
30
+
31
+ /**
32
+ * Per-request hint for `validateOnChainTx`. The middleware threads the
33
+ * verify-time `VerifyContext` through this so the synthetic on-chain
34
+ * fixture mirrors the SERVICE-supplied requirements (cross-check passes
35
+ * cleanly without parsing the actual signed tx bytes).
36
+ */
37
+ export interface DemoChainHint {
38
+ readonly payer: PublicKey;
39
+ readonly transferredAmount: bigint;
40
+ readonly transferredMint: PublicKey;
41
+ readonly transferRecipient: PublicKey;
42
+ }
43
+
44
+ export class DemoOnChainStub {
45
+ private hint: DemoChainHint | null = null;
46
+ private counter = 0;
47
+ private readonly emittedLogs = new Map<string, {
48
+ feedbackTxSignature: string;
49
+ emittedAtSlot: number;
50
+ }>();
51
+
52
+ /** Set the next-call hint. Called by the middleware right before each
53
+ * validatePaymentProof. Single-shot — cleared after consumption. */
54
+ setHint(hint: DemoChainHint): void {
55
+ this.hint = hint;
56
+ }
57
+
58
+ validateOnChainTx: ValidateOnChainTxFn = async (_txBase64) => {
59
+ const hint = this.hint;
60
+ this.hint = null;
61
+
62
+ const signature = `${SYNTHETIC_SIG_PREFIX}${++this.counter}`;
63
+ if (!hint) {
64
+ // Without a hint we can't synthesise transfer fields; surface as
65
+ // settlement_failed (matches the all-or-nothing contract).
66
+ return {
67
+ confirmed: false,
68
+ errorReason: "settlement_failed",
69
+ errorDetail: "demo stub has no chain hint",
70
+ } satisfies OnChainTxValidation;
71
+ }
72
+ return {
73
+ confirmed: true,
74
+ payer: hint.payer,
75
+ signature,
76
+ slot: 0,
77
+ transferredAmount: hint.transferredAmount,
78
+ transferredMint: hint.transferredMint,
79
+ transferRecipient: hint.transferRecipient,
80
+ } satisfies OnChainTxValidation;
81
+ };
82
+
83
+ emitFeedbackCpi = async (input: EmitFeedbackInput) => {
84
+ const key = paymentIdHashKey(input.settlement.paymentIdHash);
85
+ const prior = this.emittedLogs.get(key);
86
+ if (prior) {
87
+ const err = new Error(`paymentIdHash ${key} already in use`);
88
+ throw err;
89
+ }
90
+ const result = {
91
+ feedbackTxSignature: `${SYNTHETIC_FEEDBACK_PREFIX}${++this.counter}`,
92
+ emittedAtSlot: 0,
93
+ };
94
+ this.emittedLogs.set(key, result);
95
+ return result;
96
+ };
97
+
98
+ priorEmissionLookup: PriorEmissionLookup = async (paymentIdHash) => {
99
+ return this.emittedLogs.get(paymentIdHashKey(paymentIdHash)) ?? null;
100
+ };
101
+ }
102
+
103
+ export interface MakeDemoPayShDepsArgs {
104
+ readonly signingNetwork: string;
105
+ readonly feePayer: PublicKey;
106
+ }
107
+
108
+ export interface DemoPayShDepsBundle {
109
+ readonly deps: PayShDeps;
110
+ readonly chainStub: DemoOnChainStub;
111
+ readonly replayCache: ReplayCache;
112
+ }
113
+
114
+ export function makeDemoPayShDeps(args: MakeDemoPayShDepsArgs): DemoPayShDepsBundle {
115
+ const stub = new DemoOnChainStub();
116
+ const replayCache = new ReplayCache();
117
+ const deps: PayShDeps = {
118
+ signingNetwork: args.signingNetwork,
119
+ feePayer: args.feePayer,
120
+ validateOnChainTx: stub.validateOnChainTx,
121
+ emitFeedbackCpi: stub.emitFeedbackCpi,
122
+ priorEmissionLookup: stub.priorEmissionLookup,
123
+ replayCache,
124
+ };
125
+ return { deps, chainStub: stub, replayCache };
126
+ }
127
+
128
+ /** Build a `ConfirmedSettlement` from the proof-validation result + ctx. */
129
+ export function buildConfirmedSettlement(
130
+ ctx: VerifyContext,
131
+ txSig: string,
132
+ payer: PublicKey,
133
+ ): ConfirmedSettlement {
134
+ return {
135
+ txSignature: txSig,
136
+ payer,
137
+ payee: ctx.payeeAgent,
138
+ amount: ctx.amount,
139
+ mint: ctx.mint,
140
+ paymentIdHash: ctx.paymentIdHash ?? new Uint8Array(32),
141
+ };
142
+ }
143
+
144
+ function paymentIdHashKey(hash: Uint8Array): string {
145
+ let s = "";
146
+ for (let i = 0; i < hash.length; i++) {
147
+ s += hash[i].toString(16).padStart(2, "0");
148
+ }
149
+ return s;
150
+ }
@@ -0,0 +1,471 @@
1
+ /**
2
+ * Pay.sh + AgentTrust TrustGate live-demo Express server.
3
+ *
4
+ * Single endpoint: `GET /protected`, gated by AgentTrust.
5
+ *
6
+ * $ pay --sandbox curl http://localhost:3402/protected
7
+ *
8
+ * 1. Demo emits 402 with x402 v2 PaymentRequirements
9
+ * 2. Pay.sh CLI signs locally (Surfpool sandbox), retries
10
+ * 3. Demo runs the AgentTrust pipeline:
11
+ * adapter.parseRequest → decide(ctx, payerTier vs minTier)
12
+ * → Allow → validate proof → emit feedback → 200
13
+ * → Deny → 402 + reason headers
14
+ *
15
+ * Counterparty tiers are read from the `X-Demo-Payer-Agent` header so a
16
+ * single binary can demonstrate Allow + Deny paths without redeploying.
17
+ *
18
+ * See `examples/pay-sh-demo/README.md` for full operating notes.
19
+ */
20
+
21
+ import express, { Application, Request } from "express";
22
+ import { Keypair, PublicKey } from "@solana/web3.js";
23
+ import { createHash } from "crypto";
24
+
25
+ import {
26
+ PaySh,
27
+ PayShDeps,
28
+ ReplayCache,
29
+ bytesToHex,
30
+ canonicalChallengeBytes,
31
+ deriveMemoHash,
32
+ signEnvelope,
33
+ } from "@agenttrust/trustgate-server";
34
+
35
+ import { makeDemoPayShDeps, DemoOnChainStub } from "./deps";
36
+ import { makeRealPayShDeps, MakeRealPayShDepsArgs } from "./deps-real";
37
+ import { paymentMiddleware } from "./middleware";
38
+ import {
39
+ CounterpartyTable,
40
+ DEMO_POLICY_MIN_TIER,
41
+ LiveTierCache,
42
+ makeLiveTierDecide,
43
+ makeTierDecide,
44
+ } from "./policy";
45
+ import { buildPaymentRequirements } from "./x402";
46
+
47
+ import { Connection } from "@solana/web3.js";
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Demo constants
51
+ // ---------------------------------------------------------------------------
52
+
53
+ const DEMO_POLICY_ID = 1;
54
+ const DEMO_AMOUNT_ATOMIC = 1_000n; // 0.001 USDC at 6 decimals
55
+ const DEMO_NETWORK_DEVNET = "solana-devnet";
56
+ const DEMO_MAX_TIMEOUT_SECONDS = 60;
57
+ /** USDC mint (mainnet — Surfpool mirrors mainnet mints to localnet). */
58
+ const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Demo state factory
62
+ // ---------------------------------------------------------------------------
63
+
64
+ export interface CreateDemoAppOptions {
65
+ /** Override per-counterparty tiers. Default: 3 demo agents at tiers 0/1/3. */
66
+ readonly counterparties?: CounterpartyTable;
67
+ /** Override the policy gate's minimum tier. Default: 2. */
68
+ readonly minTier?: number;
69
+ /** Override the SPL mint. Default: USDC mainnet. */
70
+ readonly mint?: string;
71
+ /** Override the network slug. Default: solana-devnet. */
72
+ readonly network?: string;
73
+ /** Override the SPL transfer recipient (the SERVICE's ATA). Defaults to the SERVICE wallet. */
74
+ readonly payeeRecipient?: PublicKey;
75
+ /** Override the facilitator keypair. Defaults to a per-process generated one. */
76
+ readonly facilitator?: Keypair;
77
+ /** Override the SERVICE-side payee Quantu agent PDA. */
78
+ readonly payeeAgent?: PublicKey;
79
+ /** Override the SERVICE-side payee wallet. */
80
+ readonly payeeWallet?: PublicKey;
81
+ /** Override the replay cache (for tests that share state across requests). */
82
+ readonly replayCache?: ReplayCache;
83
+ }
84
+
85
+ /** Real-chain variant — wires Anchor + RPC + Quantu account resolver. */
86
+ export interface CreateRealDemoAppOptions extends CreateDemoAppOptions {
87
+ readonly realChain: Omit<MakeRealPayShDepsArgs, "facilitator">;
88
+ /**
89
+ * Phase J4 — when present, the policy gate switches from a static
90
+ * counterparty-table lookup to live `tier_immediate` reads off Quantu's
91
+ * `AtomStats` PDA, with a per-payer 60s in-process cache.
92
+ *
93
+ * The static `counterparties` table (if any) is retained as the fallback
94
+ * for unknown payers and as a stale-tier safety net under RPC failures —
95
+ * see `makeLiveTierDecide` for the full degradation rules.
96
+ */
97
+ readonly liveTier?: {
98
+ readonly resolveAtomStats: (payerAgent: PublicKey) => PublicKey | null;
99
+ readonly atomEngineId: PublicKey;
100
+ readonly ttlMs?: number;
101
+ readonly cache?: LiveTierCache;
102
+ };
103
+ }
104
+
105
+ export interface DemoApp {
106
+ readonly app: Application;
107
+ readonly facilitator: Keypair;
108
+ readonly payeeWallet: Keypair | PublicKey;
109
+ readonly counterparties: CounterpartyTable;
110
+ /** Restart-safe — invoke before / between tests to reset state. */
111
+ reset(): void;
112
+ }
113
+
114
+ export function createDemoApp(opts: CreateDemoAppOptions = {}): DemoApp {
115
+ const network = opts.network ?? DEMO_NETWORK_DEVNET;
116
+ const mint = opts.mint ?? USDC_MINT;
117
+ const facilitator = opts.facilitator ?? Keypair.generate();
118
+ const payeeKeypair = opts.payeeWallet ? undefined : Keypair.generate();
119
+ const payeeWallet = opts.payeeWallet ?? payeeKeypair!.publicKey;
120
+ const payeeAgent = opts.payeeAgent ?? deriveAgentPda(payeeWallet, "payee");
121
+ const payeeRecipient = opts.payeeRecipient ?? payeeWallet;
122
+ const counterparties = opts.counterparties ?? defaultCounterpartyTable();
123
+ const minTier = opts.minTier ?? DEMO_POLICY_MIN_TIER;
124
+
125
+ const { deps, chainStub } = makeDemoPayShDeps({
126
+ signingNetwork: network,
127
+ feePayer: facilitator.publicKey,
128
+ });
129
+ return assembleDemoApp({
130
+ network, mint, facilitator, payeeKeypair, payeeWallet, payeeAgent,
131
+ payeeRecipient, counterparties, minTier,
132
+ deps, chainStub,
133
+ replayCacheOverride: opts.replayCache,
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Real-chain variant. Loads Anchor + RPC, wires
139
+ * `makeValidateOnChainTx` + `makeEmitFeedbackCpi` +
140
+ * `makePriorEmissionLookup` from `@agenttrust-sdk/trustgate`. The
141
+ * middleware's `chainStub` setHint hook is a no-op in this mode — the
142
+ * adapter parses the actual signed tx via RPC.
143
+ */
144
+ export async function createRealDemoApp(
145
+ opts: CreateRealDemoAppOptions,
146
+ ): Promise<DemoApp> {
147
+ const network = opts.network ?? DEMO_NETWORK_DEVNET;
148
+ const mint = opts.mint ?? USDC_MINT;
149
+ const facilitator = opts.facilitator ?? Keypair.generate();
150
+ const payeeKeypair = opts.payeeWallet ? undefined : Keypair.generate();
151
+ const payeeWallet = opts.payeeWallet ?? payeeKeypair!.publicKey;
152
+ const payeeAgent = opts.payeeAgent ?? deriveAgentPda(payeeWallet, "payee");
153
+ const payeeRecipient = opts.payeeRecipient ?? payeeWallet;
154
+ const counterparties = opts.counterparties ?? defaultCounterpartyTable();
155
+ const minTier = opts.minTier ?? DEMO_POLICY_MIN_TIER;
156
+
157
+ const { deps, connection } = await makeRealPayShDeps({
158
+ ...opts.realChain,
159
+ facilitator,
160
+ signingNetwork: opts.realChain.signingNetwork ?? network,
161
+ });
162
+ // Real path doesn't use the chainStub — pass a no-op stub instance.
163
+ const chainStub = new DemoOnChainStub();
164
+
165
+ // Live-tier override: the static counterparty table becomes the
166
+ // RPC-failure fallback, and `decide` reads tier_immediate off the
167
+ // Quantu AtomStats PDA on each gate (with the J4 60s cache).
168
+ const decide = opts.liveTier
169
+ ? makeLiveTierDecide({
170
+ connection,
171
+ resolveAtomStats: opts.liveTier.resolveAtomStats,
172
+ atomEngineId: opts.liveTier.atomEngineId,
173
+ minTier,
174
+ ttlMs: opts.liveTier.ttlMs,
175
+ cache: opts.liveTier.cache,
176
+ fallbackTable: counterparties,
177
+ })
178
+ : makeTierDecide(counterparties, minTier);
179
+
180
+ return assembleDemoApp({
181
+ network, mint, facilitator, payeeKeypair, payeeWallet, payeeAgent,
182
+ payeeRecipient, counterparties, minTier,
183
+ deps, chainStub,
184
+ replayCacheOverride: opts.replayCache,
185
+ decideOverride: decide,
186
+ });
187
+ }
188
+
189
+ interface AssembleArgs {
190
+ readonly network: string;
191
+ readonly mint: string;
192
+ readonly facilitator: Keypair;
193
+ readonly payeeKeypair?: Keypair;
194
+ readonly payeeWallet: PublicKey;
195
+ readonly payeeAgent: PublicKey;
196
+ readonly payeeRecipient: PublicKey;
197
+ readonly counterparties: CounterpartyTable;
198
+ readonly minTier: number;
199
+ readonly deps: PayShDeps;
200
+ readonly chainStub: DemoOnChainStub;
201
+ readonly replayCacheOverride?: ReplayCache;
202
+ /** Phase J4 — when set, replaces the default static-table decide. */
203
+ readonly decideOverride?: (ctx: import("@agenttrust/trustgate-server").VerifyContext) => Promise<import("@agenttrust/trustgate-server").GateDecision>;
204
+ }
205
+
206
+ function assembleDemoApp(args: AssembleArgs): DemoApp {
207
+ const {
208
+ network, mint, facilitator, payeeKeypair, payeeWallet, payeeAgent,
209
+ payeeRecipient, counterparties, minTier, deps, chainStub,
210
+ replayCacheOverride,
211
+ } = args;
212
+
213
+ const adapter = new PaySh({
214
+ ...deps,
215
+ replayCache: replayCacheOverride ?? deps.replayCache,
216
+ });
217
+
218
+ const decide = args.decideOverride ?? makeTierDecide(counterparties, minTier);
219
+
220
+ const memoToHashHex = (memo: string): string => bytesToHex(deriveMemoHash(memo));
221
+
222
+ const agentTrustFor = (req: Request) => {
223
+ const payerHint = (req.header("X-Demo-Payer-Agent") ?? "").trim();
224
+ const fallbackEntry = counterparties.keys().next().value;
225
+ const payerB58 = payerHint.length > 0
226
+ ? payerHint
227
+ : fallbackEntry ?? payeeAgent.toBase58();
228
+ return {
229
+ payerAgentAsset: payerB58,
230
+ payeeAgentAsset: payeeAgent.toBase58(),
231
+ payeeRecipient: payeeRecipient.toBase58(),
232
+ policyId: DEMO_POLICY_ID,
233
+ };
234
+ };
235
+
236
+ const buildForRequest = buildPaymentRequirements({
237
+ scheme: "exact",
238
+ network,
239
+ amount: DEMO_AMOUNT_ATOMIC,
240
+ asset: mint,
241
+ payTo: payeeRecipient.toBase58(),
242
+ resource: "/protected",
243
+ description: "AgentTrust demo — counterparty-tier gated resource",
244
+ maxTimeoutSeconds: DEMO_MAX_TIMEOUT_SECONDS,
245
+ feePayer: facilitator.publicKey.toBase58(),
246
+ agentTrustFor,
247
+ memoFor: deriveDemoMemo,
248
+ signChallenge: (bytes) => signEnvelope(bytes, facilitator.secretKey),
249
+ canonicalBytesOf: canonicalChallengeBytes,
250
+ bytesToHex,
251
+ paymentIdHashHexFor: memoToHashHex,
252
+ });
253
+
254
+ const app = express();
255
+ app.use(express.json({ limit: "256kb" }));
256
+
257
+ app.get("/health", (_req, res) => {
258
+ res.json({
259
+ status: "ok",
260
+ network,
261
+ facilitator: facilitator.publicKey.toBase58(),
262
+ payeeWallet: payeeWallet.toBase58(),
263
+ payeeAgent: payeeAgent.toBase58(),
264
+ counterparties: Array.from(counterparties.entries())
265
+ .map(([agent, e]) => ({ agent, tier: e.tier, label: e.label })),
266
+ minTier,
267
+ policyId: DEMO_POLICY_ID,
268
+ });
269
+ });
270
+
271
+ app.get(
272
+ "/protected",
273
+ paymentMiddleware({
274
+ adapter,
275
+ chainStub,
276
+ decide,
277
+ buildPaymentRequirements: buildForRequest,
278
+ facilitator: facilitator.publicKey,
279
+ network,
280
+ }),
281
+ (_req, res) => {
282
+ res.json({
283
+ ok: true,
284
+ message: "AgentTrust allowed this counterparty — protected resource served.",
285
+ });
286
+ },
287
+ );
288
+
289
+ return {
290
+ app,
291
+ facilitator,
292
+ payeeWallet: payeeKeypair ?? payeeWallet,
293
+ counterparties,
294
+ reset: () => { /* chain stub state is request-scoped; nothing global to reset today */ },
295
+ };
296
+ }
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Bootstrap (CLI)
300
+ // ---------------------------------------------------------------------------
301
+
302
+ if (require.main === module) {
303
+ const port = Number(process.env.PORT ?? 3402);
304
+ const network = process.env.NETWORK ?? DEMO_NETWORK_DEVNET;
305
+ const mint = process.env.MINT ?? USDC_MINT;
306
+
307
+ // Real-chain mode when a facilitator keypair is supplied; falls back
308
+ // to the in-memory mock when missing (local dev / unit smoke).
309
+ const facilitatorB58 = process.env.FACILITATOR_KEYPAIR_B58;
310
+ if (facilitatorB58) {
311
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
312
+ const bs58 = require("bs58").default;
313
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
314
+ const fs = require("fs");
315
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
316
+ const path = require("path");
317
+ const facilitator = Keypair.fromSecretKey(bs58.decode(facilitatorB58));
318
+
319
+ // Load the bundled trustgate IDL (Phase F1: anchor 0.31 cannot
320
+ // deserialise the on-chain IDL deployed by anchor CLI 1.0). Walk
321
+ // up the tree the same way as the counterparty map.
322
+ const idlCandidates: string[] = [];
323
+ for (let depth = 0; depth <= 5; depth++) {
324
+ const dir = path.resolve(__dirname, ...new Array(depth).fill(".."));
325
+ idlCandidates.push(path.join(dir, "idl", "trustgate.json"));
326
+ idlCandidates.push(path.join(dir, "src", "idl", "trustgate.json"));
327
+ }
328
+ let trustgateIdl: unknown = undefined;
329
+ for (const p of idlCandidates) {
330
+ if (fs.existsSync(p)) {
331
+ trustgateIdl = JSON.parse(fs.readFileSync(p, "utf-8"));
332
+ break;
333
+ }
334
+ }
335
+
336
+ // Counterparty map → Quantu resolver. The demo bundles
337
+ // devnet-counterparties.json at the workspace root.
338
+ // Walk up the directory tree from __dirname looking for the
339
+ // file — works for both ts-node dev (src/) and node prod
340
+ // (dist/src/) without hard-coding hop counts.
341
+ const cpCandidates: string[] = [];
342
+ for (let depth = 0; depth <= 5; depth++) {
343
+ const dir = path.resolve(__dirname, ...new Array(depth).fill(".."));
344
+ cpCandidates.push(path.join(dir, "devnet-counterparties.json"));
345
+ }
346
+ cpCandidates.push(path.resolve(process.cwd(), "examples/pay-sh-demo/devnet-counterparties.json"));
347
+ let counterpartyMap: { baseCollection: string; counterparties: Array<{ asset: string; agentAccount: string; atomStats: string }> } | null = null;
348
+ for (const p of cpCandidates) {
349
+ if (fs.existsSync(p)) {
350
+ counterpartyMap = JSON.parse(fs.readFileSync(p, "utf-8"));
351
+ break;
352
+ }
353
+ }
354
+ if (!counterpartyMap) {
355
+ // eslint-disable-next-line no-console
356
+ console.error("FATAL: devnet-counterparties.json missing; demo can't resolve Quantu accounts");
357
+ process.exit(1);
358
+ }
359
+
360
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
361
+ const { PublicKey } = require("@solana/web3.js");
362
+ const collection = new PublicKey(counterpartyMap.baseCollection);
363
+ const byAgent = new Map<string, { asset: string; atomStats: string }>();
364
+ for (const cp of counterpartyMap.counterparties) byAgent.set(cp.agentAccount, cp);
365
+
366
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
367
+ const sdk = require("@agenttrust-sdk/trustgate");
368
+ const resolveQuantu = async (payeeAgent: PublicKey) => {
369
+ const e = byAgent.get(payeeAgent.toBase58());
370
+ if (!e) throw new Error(`unknown payee ${payeeAgent.toBase58()}`);
371
+ return {
372
+ agentAccount: payeeAgent,
373
+ asset: new PublicKey(e.asset),
374
+ collection,
375
+ atomConfig: sdk.deriveAtomConfigPda(sdk.DEFAULT_DEVNET_QUANTU_IDS),
376
+ atomStats: new PublicKey(e.atomStats),
377
+ atomEngineProgram: sdk.DEFAULT_DEVNET_QUANTU_IDS.atomEngine,
378
+ registryAuthority: sdk.deriveAtomRegistryAuthorityPda(sdk.DEFAULT_DEVNET_QUANTU_IDS),
379
+ };
380
+ };
381
+
382
+ // Live-tier resolver: walks the same counterparty map already loaded
383
+ // for emit_feedback. Maps payerAgent → atom_stats. Returns null for
384
+ // unknown payers so makeLiveTierDecide can fall back to the static
385
+ // counterparty table (preserving the v1 demo's tier 0 / 1 / 3 fixture
386
+ // for synthetic payer headers).
387
+ const resolveAtomStats = (payerAgent: PublicKey): PublicKey | null => {
388
+ const e = byAgent.get(payerAgent.toBase58());
389
+ return e ? new PublicKey(e.atomStats) : null;
390
+ };
391
+
392
+ createRealDemoApp({
393
+ network, mint, facilitator,
394
+ realChain: {
395
+ rpcUrl: process.env.RPC_URL ?? "https://api.devnet.solana.com",
396
+ signingNetwork: network,
397
+ resolveQuantu,
398
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
399
+ trustgateIdl: trustgateIdl as any,
400
+ },
401
+ liveTier: {
402
+ resolveAtomStats,
403
+ atomEngineId: sdk.DEFAULT_DEVNET_QUANTU_IDS.atomEngine,
404
+ },
405
+ }).then((demo) => {
406
+ demo.app.listen(port, () => {
407
+ // eslint-disable-next-line no-console
408
+ console.log(`agenttrust-pay-sh-demo (real-chain) listening on :${port}`);
409
+ // eslint-disable-next-line no-console
410
+ console.log(` facilitator=${facilitator.publicKey.toBase58()}, network=${network}`);
411
+ });
412
+ }).catch((err) => {
413
+ // eslint-disable-next-line no-console
414
+ console.error("real-chain boot failed:", err);
415
+ process.exit(1);
416
+ });
417
+ } else {
418
+ const demo = createDemoApp({ network, mint });
419
+ demo.app.listen(port, () => {
420
+ // eslint-disable-next-line no-console
421
+ console.log(`agenttrust-pay-sh-demo (mock-chain) listening on :${port}`);
422
+ // eslint-disable-next-line no-console
423
+ console.log(` facilitator=${demo.facilitator.publicKey.toBase58()}`);
424
+ // eslint-disable-next-line no-console
425
+ console.log(` Try: pay --sandbox curl http://localhost:${port}/protected`);
426
+ });
427
+ }
428
+ }
429
+
430
+ // ---------------------------------------------------------------------------
431
+ // Helpers
432
+ // ---------------------------------------------------------------------------
433
+
434
+ interface CounterpartyTableEntryExternal {
435
+ readonly agent: string;
436
+ readonly tier: number;
437
+ readonly label: string;
438
+ }
439
+
440
+ function defaultCounterpartyTable(): CounterpartyTable {
441
+ // Three deterministic demo agent PDAs at tiers 0 / 1 / 3
442
+ const seed = (label: string) => {
443
+ const bytes = new Uint8Array(32);
444
+ const sha = createHash("sha256").update(`agenttrust-demo:${label}`).digest();
445
+ bytes.set(sha.subarray(0, 32));
446
+ return new PublicKey(bytes);
447
+ };
448
+ const entries: ReadonlyArray<CounterpartyTableEntryExternal> = [
449
+ { agent: seed("tier0").toBase58(), tier: 0, label: "untrusted" },
450
+ { agent: seed("tier1").toBase58(), tier: 1, label: "low-trust" },
451
+ { agent: seed("tier3").toBase58(), tier: 3, label: "trusted" },
452
+ ];
453
+ return new Map(entries.map(({ agent, tier, label }) => [agent, { tier, label }]));
454
+ }
455
+
456
+ function deriveAgentPda(wallet: PublicKey, suffix: string): PublicKey {
457
+ const sha = createHash("sha256")
458
+ .update(`demo-agent:${wallet.toBase58()}:${suffix}`)
459
+ .digest();
460
+ return new PublicKey(sha.subarray(0, 32));
461
+ }
462
+
463
+ function deriveDemoMemo(req: Request): string {
464
+ // Stable per-request memo so retries hit the same on-chain bucket.
465
+ // Combines path + headers Pay.sh's CLI keeps stable across retries.
466
+ const digest = createHash("sha256")
467
+ .update(`demo-memo:${req.method}:${req.originalUrl}:${req.header("user-agent") ?? ""}`)
468
+ .update(req.header("X-Demo-Payer-Agent") ?? "")
469
+ .digest("hex");
470
+ return digest;
471
+ }