xytara 3.1.5 → 3.1.7

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.
package/RELEASE_NOTES.md CHANGED
@@ -1,11 +1,11 @@
1
- `xytara` 3.1.5 adds native Coinbase CDP facilitator support, unlocking Base mainnet settlement.
1
+ `xytara` 3.1.7 completes the Coinbase CDP facilitator path, proven with a real Base mainnet settlement.
2
2
 
3
- The x402_facilitator mode now authenticates to facilitators that require a short-lived CDP Bearer token, so the runtime can settle real USDC on Base mainnet through the CDP-hosted facilitator, not just test USDC on Base Sepolia via the default facilitator.
3
+ A facilitator can signal a verification or settlement result either with a 200 and the result in the body, or with a non-2xx status and the same body. Coinbase CDP returns HTTP 400 for an invalid payment (with `{isValid:false, invalidReason, invalidMessage}`). The client previously treated any non-2xx as an unreachable transport error and never read the body, so a CDP verdict was misclassified. This release reads the result from the body regardless of HTTP status, which was the last blocker for Base mainnet settlement through CDP.
4
4
 
5
5
  Highlights:
6
6
 
7
- - the facilitator client mints a fresh EdDSA (Ed25519) CDP Bearer JWT per `/verify` and `/settle` request, with the `uri` claim bound to each request and a 120-second expiry, because CDP rejects static tokens
8
- - enable it by setting `CDP_API_KEY_ID` and `CDP_API_KEY_SECRET` (or `XYTARA_X402_CDP_KEY_ID` / `XYTARA_X402_CDP_KEY_SECRET`) and pointing `XYTARA_X402_FACILITATOR_URL` at the CDP facilitator; the Ed25519 secret is the base64 64-byte key CDP issues
9
- - zero new dependencies: the JWT is signed with Node's built-in Ed25519
10
- - when no CDP key is configured, behavior is unchanged: static-header facilitators and the default Base Sepolia path work exactly as before
11
- - adds `npm run verify:cdp-facilitator`, which checks the per-request token minting, claim shape, and signature against a mock facilitator, and documents the Base mainnet setup in `REAL_PAYMENT_SETUP.md`
7
+ - the facilitator client treats any `/verify` or `/settle` response that carries an `isValid` or `success` flag as the facilitator's result, regardless of HTTP status; only a missing or unparseable body is a transport failure
8
+ - failure details now surface the facilitator's `invalidReason` / `invalidMessage` / `errorReason` and a truncated response-body excerpt, so operators can see exactly why a facilitator rejected a payment
9
+ - adds regression checks for facilitators that return a non-2xx status with a result body
10
+ - no change for facilitators that return 200 with the result; the default Base Sepolia path is unaffected
11
+ - verified end to end: a real Base mainnet USDC payment settled on-chain through the CDP facilitator and minted self-serve credits
@@ -6,9 +6,9 @@
6
6
  "source_ref": "spine.job.commitment",
7
7
  "committed_ref": "transaction.job.commitment",
8
8
  "precommit_pack_ref": "precommit.job.commitment.readiness",
9
- "transaction_id": "txn_18742578ec28",
10
- "receipt_id": "rcpt_fa3b4219d3c4",
11
- "proof_ref": "proof_rcpt_fa3b4219d3c4",
9
+ "transaction_id": "txn_2b8e48010d89",
10
+ "receipt_id": "rcpt_5b59fa10c38d",
11
+ "proof_ref": "proof_rcpt_5b59fa10c38d",
12
12
  "payment_protocol": "x402",
13
13
  "settlement_mode": "evm_payment",
14
14
  "treasury_destination_id": "treasury_acct_demo",
@@ -98,7 +98,7 @@ function createFacilitatorClient(config) {
98
98
  } catch (_) {
99
99
  json = null;
100
100
  }
101
- return { ok: response.ok, status: response.status, json };
101
+ return { ok: response.ok, status: response.status, json, bodyExcerpt: text ? text.slice(0, 400) : "" };
102
102
  } catch (error) {
103
103
  return { ok: false, status: 0, json: null, error: error && error.message ? error.message : "facilitator_request_failed" };
104
104
  } finally {
@@ -127,13 +127,25 @@ async function verifyX402Payment({ paymentHeader, accepts, resourceUrl, facilita
127
127
  if (!paymentRequirements) {
128
128
  return { ok: false, status: 402, reason: "payment_requirements_mismatch", detail: "accepted requirements do not match any advertised payment option" };
129
129
  }
130
- const requirementsForFacilitator = { ...paymentRequirements, resource: resourceUrl };
130
+ // The x402 v2 PaymentRequirements shape is exactly {scheme, network, asset, amount, payTo,
131
+ // maxTimeoutSeconds, extra} with no `resource` field (resource lives on the payload in v2).
132
+ // Adding one makes strict facilitators (e.g. Coinbase CDP) reject the request with HTTP 400
133
+ // and makes paymentRequirements diverge from paymentPayload.accepted, so send it clean.
134
+ // resourceUrl is accepted for API compatibility but intentionally not sent to the facilitator.
135
+ void resourceUrl;
136
+ const requirementsForFacilitator = { ...paymentRequirements };
131
137
  const verification = await facilitator.verify(paymentPayload, requirementsForFacilitator);
132
- if (!verification.ok || !verification.json) {
133
- return { ok: false, status: 402, reason: "facilitator_unreachable", detail: verification.error || `facilitator verify returned HTTP ${verification.status}` };
138
+ // A facilitator signals an invalid payment either with a 200 and isValid:false (x402.org) or
139
+ // with a non-2xx status and the same body (Coinbase CDP returns HTTP 400). Treat any response
140
+ // that carries an isValid flag as a verification result; only a missing/garbled body is a
141
+ // genuine transport failure.
142
+ if (!verification.json || typeof verification.json.isValid === "undefined") {
143
+ const body = verification.bodyExcerpt ? ` body=${verification.bodyExcerpt}` : "";
144
+ return { ok: false, status: 402, reason: "facilitator_unreachable", detail: verification.error || `facilitator verify returned HTTP ${verification.status}${body}` };
134
145
  }
135
146
  if (verification.json.isValid !== true) {
136
- return { ok: false, status: 402, reason: "payment_verification_failed", detail: normalizeString(verification.json.invalidReason) || "facilitator rejected the payment" };
147
+ const detail = normalizeString(verification.json.invalidReason) || normalizeString(verification.json.invalidMessage) || "facilitator rejected the payment";
148
+ return { ok: false, status: 402, reason: "payment_verification_failed", detail };
137
149
  }
138
150
  return {
139
151
  ok: true,
@@ -152,11 +164,15 @@ async function settleX402Payment({ authorization, facilitator, settle }) {
152
164
  return { ok: true, settlement: { success: true, skipped: true, reason: "settlement_disabled_by_operator", payer: authorization.payer } };
153
165
  }
154
166
  const settlement = await facilitator.settle(authorization.paymentPayload, authorization.paymentRequirements);
155
- if (!settlement.ok || !settlement.json) {
156
- return { ok: false, status: 402, reason: "facilitator_unreachable", detail: settlement.error || `facilitator settle returned HTTP ${settlement.status}` };
167
+ // As with verify, a facilitator may report a failed settlement with a non-2xx status and a
168
+ // body carrying success:false; only a missing/garbled body is a genuine transport failure.
169
+ if (!settlement.json || typeof settlement.json.success === "undefined") {
170
+ const body = settlement.bodyExcerpt ? ` body=${settlement.bodyExcerpt}` : "";
171
+ return { ok: false, status: 402, reason: "facilitator_unreachable", detail: settlement.error || `facilitator settle returned HTTP ${settlement.status}${body}` };
157
172
  }
158
173
  if (settlement.json.success !== true) {
159
- return { ok: false, status: 402, reason: "payment_settlement_failed", detail: normalizeString(settlement.json.errorReason) || "facilitator could not settle the payment" };
174
+ const detail = normalizeString(settlement.json.errorReason) || normalizeString(settlement.json.errorMessage) || "facilitator could not settle the payment";
175
+ return { ok: false, status: 402, reason: "payment_settlement_failed", detail };
160
176
  }
161
177
  return { ok: true, settlement: settlement.json };
162
178
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xytara",
3
- "version": "3.1.5",
3
+ "version": "3.1.7",
4
4
  "description": "Agent-commerce runtime for quote, pay, execute, deliver, meter, and integrate.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,194 +1,216 @@
1
- "use strict";
2
-
3
- // Verifies the x402_facilitator payment mode end to end against a mock facilitator:
4
- // - an unpaid x402 execute returns 402 and advertises the configured accepts
5
- // - a well-formed payment is verified and settled, the command runs, and the
6
- // response carries a PAYMENT-RESPONSE settlement receipt
7
- // - malformed, mismatched, facilitator-rejected, and unsettled payments all 402
8
- //
9
- // No real network or chain is used; the mock facilitator stands in for x402.org.
10
-
11
- const assert = require("assert");
12
- const http = require("http");
13
-
14
- const ACCEPTS = [
15
- {
16
- scheme: "exact",
17
- network: "eip155:84532",
18
- asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
19
- amount: "1000",
20
- payTo: "0x7cd2E0Ed2721319B20FAac4109774d030833F84D",
21
- extra: { name: "USDC", version: "2" },
22
- maxTimeoutSeconds: 60
23
- }
24
- ];
25
-
26
- function startMockFacilitator() {
27
- const calls = { verify: 0, settle: 0 };
28
- const server = http.createServer((req, res) => {
29
- const chunks = [];
30
- req.on("data", (c) => chunks.push(c));
31
- req.on("end", () => {
32
- let body = {};
33
- try { body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); } catch (_) { body = {}; }
34
- const sig = body && body.paymentPayload && body.paymentPayload.payload ? String(body.paymentPayload.payload.signature || "") : "";
35
- const reply = (payload) => {
36
- const text = JSON.stringify(payload);
37
- res.writeHead(200, { "content-type": "application/json", "content-length": Buffer.byteLength(text) });
38
- res.end(text);
39
- };
40
- if (req.method === "POST" && req.url === "/verify") {
41
- calls.verify += 1;
42
- if (sig === "reject-me") return reply({ isValid: false, invalidReason: "invalid_signature" });
43
- return reply({ isValid: true, payer: "0x1111111111111111111111111111111111111111" });
44
- }
45
- if (req.method === "POST" && req.url === "/settle") {
46
- calls.settle += 1;
47
- if (sig === "settle-fails") return reply({ success: false, errorReason: "insufficient_funds" });
48
- return reply({ success: true, transaction: "0xmocktx", network: "eip155:84532", payer: "0x1111111111111111111111111111111111111111" });
49
- }
50
- res.writeHead(404); res.end();
51
- });
52
- });
53
- return new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve({ server, calls, port: server.address().port })));
54
- }
55
-
56
- function paymentHeader(signature, overrides = {}) {
57
- const accepted = ACCEPTS[0];
58
- return Buffer.from(JSON.stringify({
59
- x402Version: 2,
60
- accepted: { ...accepted, ...overrides.accepted },
61
- payload: {
62
- signature,
63
- authorization: {
64
- from: "0x1111111111111111111111111111111111111111",
65
- to: accepted.payTo,
66
- value: accepted.amount,
67
- validAfter: "0",
68
- validBefore: String(Math.floor(Date.now() / 1000) + 600),
69
- nonce: `0x${"ab".repeat(32)}`
70
- }
71
- }
72
- }), "utf8").toString("base64");
73
- }
74
-
75
- async function post(baseUrl, path, body, headers = {}) {
76
- const res = await fetch(`${baseUrl}${path}`, {
77
- method: "POST",
78
- headers: { "content-type": "application/json", ...headers },
79
- body: JSON.stringify(body)
80
- });
81
- let json = null;
82
- try { json = await res.json(); } catch (_) { json = null; }
83
- return { status: res.status, json, headers: res.headers };
84
- }
85
-
86
- async function main() {
87
- const facilitator = await startMockFacilitator();
88
- const previous = {
89
- mode: process.env.XYTARA_PAYMENT_VERIFICATION_MODE,
90
- accepts: process.env.XYTARA_X402_ACCEPTS_JSON,
91
- facUrl: process.env.XYTARA_X402_FACILITATOR_URL
92
- };
93
- process.env.XYTARA_PAYMENT_VERIFICATION_MODE = "x402_facilitator";
94
- process.env.XYTARA_X402_ACCEPTS_JSON = JSON.stringify(ACCEPTS);
95
- process.env.XYTARA_X402_FACILITATOR_URL = `http://127.0.0.1:${facilitator.port}`;
96
-
97
- delete require.cache[require.resolve("../server")];
98
- const { createServer } = require("../server");
99
- const server = createServer();
100
- await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
101
- const baseUrl = `http://127.0.0.1:${server.address().port}`;
102
- const execBody = { account_id: "acct_x402", command: "x402 facilitator demo", task_ref: "trust.verify", body: { subject_id: "s-1" } };
103
-
104
- try {
105
- // 1. Unpaid -> 402 advertising accepts.
106
- const unpaid = await post(baseUrl, "/x402/commands/execute", execBody);
107
- assert.strictEqual(unpaid.status, 402, "unpaid x402 execute must return 402");
108
- const required = unpaid.json && unpaid.json.payment_required;
109
- assert.ok(required, "402 must carry payment_required");
110
- assert.strictEqual(required.x402Version, 2, "challenge must be x402 v2");
111
- assert.ok(Array.isArray(required.accepts) && required.accepts.length === 1, "challenge must advertise accepts");
112
- assert.strictEqual(required.accepts[0].network, "eip155:84532", "advertised network mismatch");
113
- assert.strictEqual(typeof unpaid.headers.get("payment-required"), "string", "PAYMENT-REQUIRED header missing");
114
-
115
- // 2. Bogus header -> 402 payment_signature_invalid.
116
- const bogus = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": "not-a-payment" });
117
- assert.strictEqual(bogus.status, 402, "bogus payment header must 402");
118
- assert.strictEqual(bogus.json.reason, "payment_signature_invalid", "bogus reason mismatch");
119
-
120
- // 3. Mismatched requirements -> 402 before the facilitator.
121
- const verifyBefore = facilitator.calls.verify;
122
- const mismatch = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("ok", { accepted: { amount: "9" } }) });
123
- assert.strictEqual(mismatch.status, 402, "mismatched requirements must 402");
124
- assert.strictEqual(mismatch.json.reason, "payment_requirements_mismatch", "mismatch reason");
125
- assert.strictEqual(facilitator.calls.verify, verifyBefore, "facilitator must not be called on a requirements mismatch");
126
-
127
- // 4. Facilitator rejects -> 402 payment_verification_failed.
128
- const rejected = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("reject-me") });
129
- assert.strictEqual(rejected.status, 402, "facilitator-rejected payment must 402");
130
- assert.strictEqual(rejected.json.reason, "payment_verification_failed", "rejection reason");
131
- assert.strictEqual(rejected.json.detail, "invalid_signature", "rejection detail");
132
-
133
- // 5. Settlement fails -> 402, no tool output.
134
- const settleFails = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("settle-fails") });
135
- assert.strictEqual(settleFails.status, 402, "failed settlement must 402");
136
- assert.strictEqual(settleFails.json.reason, "payment_settlement_failed", "settlement failure reason");
137
-
138
- // 6. Verified + settled -> 200 with a result and a PAYMENT-RESPONSE receipt.
139
- const settleBefore = facilitator.calls.settle;
140
- const paid = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("ok") });
141
- assert.strictEqual(paid.status, 200, "verified+settled payment must 200");
142
- assert.strictEqual(typeof paid.json.transaction.transaction_id, "string", "paid result missing transaction");
143
- assert.strictEqual(paid.json.transaction.status, "completed", "paid transaction must be completed");
144
- assert.strictEqual(paid.json.transaction.payment.verification_mode, "x402_facilitator", "verification mode not recorded");
145
- assert.strictEqual(facilitator.calls.settle, settleBefore + 1, "facilitator settle must be called once");
146
- const receiptHeader = paid.headers.get("payment-response");
147
- assert.strictEqual(typeof receiptHeader, "string", "PAYMENT-RESPONSE header missing on a settled 200");
148
- const receipt = JSON.parse(Buffer.from(receiptHeader, "base64").toString("utf8"));
149
- assert.strictEqual(receipt.success, true, "settlement receipt must report success");
150
- assert.strictEqual(receipt.transaction, "0xmocktx", "settlement receipt transaction mismatch");
151
-
152
- // 7. Native routes must not be disabled by x402_facilitator: a native paid attempt
153
- // falls back to local_signed semantics, never payment_verification_mode_unsupported.
154
- const nativeAttempt = await post(baseUrl, "/v1/commands/execute", execBody, {
155
- "x-wallet-id": "wallet-not-registered",
156
- "x-payment": "encoded",
157
- "x-payment-signature": "sig"
158
- });
159
- assert.notStrictEqual(nativeAttempt.json && nativeAttempt.json.reason, "payment_verification_mode_unsupported", "native route must fall back to local_signed under x402_facilitator");
160
-
161
- // 8. Self-serve credits top-up: unpaid -> 402 advertising accepts + credits_per_topup.
162
- const topupUnpaid = await post(baseUrl, "/x402/credits/topup", { account_id: "acct_topup" });
163
- assert.strictEqual(topupUnpaid.status, 402, "unpaid topup must 402");
164
- assert.ok(Array.isArray(topupUnpaid.json.payment_required.accepts), "topup 402 must advertise accepts");
165
- assert.strictEqual(typeof topupUnpaid.json.payment_required.credits_per_topup, "number", "topup 402 must state credits_per_topup");
166
-
167
- // 9. Paid top-up -> 200, credits minted to the account, with a settlement receipt.
168
- const topupPaid = await post(baseUrl, "/x402/credits/topup", { account_id: "acct_topup" }, { "payment-signature": paymentHeader("ok") });
169
- assert.strictEqual(topupPaid.status, 200, "paid topup must 200");
170
- assert.strictEqual(topupPaid.json.ok, true, "paid topup must be ok");
171
- assert.strictEqual(typeof topupPaid.json.credit_grant, "object", "paid topup must return a credit grant");
172
- const grantedUnits = Number(topupPaid.json.balance.available_units);
173
- assert.ok(grantedUnits > 0, "paid topup must leave a positive credit balance");
174
- assert.strictEqual(typeof topupPaid.headers.get("payment-response"), "string", "paid topup must carry a settlement receipt");
175
-
176
- // 10. Replayed top-up (same payment nonce) is idempotent: no double-mint.
177
- const topupReplay = await post(baseUrl, "/x402/credits/topup", { account_id: "acct_topup" }, { "payment-signature": paymentHeader("ok") });
178
- assert.strictEqual(topupReplay.status, 200, "replayed topup must still 200");
179
- assert.strictEqual(Number(topupReplay.json.balance.available_units), grantedUnits, "replayed topup must not mint credits twice");
180
-
181
- console.log("xytara verify:x402-facilitator passed");
182
- } finally {
183
- await new Promise((resolve) => server.close(resolve));
184
- await new Promise((resolve) => facilitator.server.close(resolve));
185
- if (previous.mode === undefined) delete process.env.XYTARA_PAYMENT_VERIFICATION_MODE; else process.env.XYTARA_PAYMENT_VERIFICATION_MODE = previous.mode;
186
- if (previous.accepts === undefined) delete process.env.XYTARA_X402_ACCEPTS_JSON; else process.env.XYTARA_X402_ACCEPTS_JSON = previous.accepts;
187
- if (previous.facUrl === undefined) delete process.env.XYTARA_X402_FACILITATOR_URL; else process.env.XYTARA_X402_FACILITATOR_URL = previous.facUrl;
188
- }
189
- }
190
-
191
- main().catch((error) => {
192
- console.error(error && error.message ? error.message : error);
193
- process.exitCode = 1;
194
- });
1
+ "use strict";
2
+
3
+ // Verifies the x402_facilitator payment mode end to end against a mock facilitator:
4
+ // - an unpaid x402 execute returns 402 and advertises the configured accepts
5
+ // - a well-formed payment is verified and settled, the command runs, and the
6
+ // response carries a PAYMENT-RESPONSE settlement receipt
7
+ // - malformed, mismatched, facilitator-rejected, and unsettled payments all 402
8
+ //
9
+ // No real network or chain is used; the mock facilitator stands in for x402.org.
10
+
11
+ const assert = require("assert");
12
+ const http = require("http");
13
+
14
+ const ACCEPTS = [
15
+ {
16
+ scheme: "exact",
17
+ network: "eip155:84532",
18
+ asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
19
+ amount: "1000",
20
+ payTo: "0x7cd2E0Ed2721319B20FAac4109774d030833F84D",
21
+ extra: { name: "USDC", version: "2" },
22
+ maxTimeoutSeconds: 60
23
+ }
24
+ ];
25
+
26
+ function startMockFacilitator() {
27
+ const calls = { verify: 0, settle: 0 };
28
+ const server = http.createServer((req, res) => {
29
+ const chunks = [];
30
+ req.on("data", (c) => chunks.push(c));
31
+ req.on("end", () => {
32
+ let body = {};
33
+ try { body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); } catch (_) { body = {}; }
34
+ const sig = body && body.paymentPayload && body.paymentPayload.payload ? String(body.paymentPayload.payload.signature || "") : "";
35
+ const reply = (payload, status) => {
36
+ const text = JSON.stringify(payload);
37
+ res.writeHead(status || 200, { "content-type": "application/json", "content-length": Buffer.byteLength(text) });
38
+ res.end(text);
39
+ };
40
+ if (req.method === "POST" && req.url === "/verify") {
41
+ calls.verify += 1;
42
+ calls.lastVerifyBody = body;
43
+ if (sig === "reject-me") return reply({ isValid: false, invalidReason: "invalid_signature" });
44
+ if (sig === "cdp-invalid") return reply({ isValid: false, invalidReason: "invalid_payload", invalidMessage: "contract call failed: execution reverted", payer: "0x1111111111111111111111111111111111111111" }, 400);
45
+ return reply({ isValid: true, payer: "0x1111111111111111111111111111111111111111" });
46
+ }
47
+ if (req.method === "POST" && req.url === "/settle") {
48
+ calls.settle += 1;
49
+ if (sig === "settle-fails") return reply({ success: false, errorReason: "insufficient_funds" });
50
+ if (sig === "cdp-settle-fail") return reply({ success: false, errorReason: "insufficient_funds" }, 400);
51
+ return reply({ success: true, transaction: "0xmocktx", network: "eip155:84532", payer: "0x1111111111111111111111111111111111111111" });
52
+ }
53
+ res.writeHead(404); res.end();
54
+ });
55
+ });
56
+ return new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve({ server, calls, port: server.address().port })));
57
+ }
58
+
59
+ function paymentHeader(signature, overrides = {}) {
60
+ const accepted = ACCEPTS[0];
61
+ return Buffer.from(JSON.stringify({
62
+ x402Version: 2,
63
+ accepted: { ...accepted, ...overrides.accepted },
64
+ payload: {
65
+ signature,
66
+ authorization: {
67
+ from: "0x1111111111111111111111111111111111111111",
68
+ to: accepted.payTo,
69
+ value: accepted.amount,
70
+ validAfter: "0",
71
+ validBefore: String(Math.floor(Date.now() / 1000) + 600),
72
+ nonce: `0x${"ab".repeat(32)}`
73
+ }
74
+ }
75
+ }), "utf8").toString("base64");
76
+ }
77
+
78
+ async function post(baseUrl, path, body, headers = {}) {
79
+ const res = await fetch(`${baseUrl}${path}`, {
80
+ method: "POST",
81
+ headers: { "content-type": "application/json", ...headers },
82
+ body: JSON.stringify(body)
83
+ });
84
+ let json = null;
85
+ try { json = await res.json(); } catch (_) { json = null; }
86
+ return { status: res.status, json, headers: res.headers };
87
+ }
88
+
89
+ async function main() {
90
+ const facilitator = await startMockFacilitator();
91
+ const previous = {
92
+ mode: process.env.XYTARA_PAYMENT_VERIFICATION_MODE,
93
+ accepts: process.env.XYTARA_X402_ACCEPTS_JSON,
94
+ facUrl: process.env.XYTARA_X402_FACILITATOR_URL
95
+ };
96
+ process.env.XYTARA_PAYMENT_VERIFICATION_MODE = "x402_facilitator";
97
+ process.env.XYTARA_X402_ACCEPTS_JSON = JSON.stringify(ACCEPTS);
98
+ process.env.XYTARA_X402_FACILITATOR_URL = `http://127.0.0.1:${facilitator.port}`;
99
+
100
+ delete require.cache[require.resolve("../server")];
101
+ const { createServer } = require("../server");
102
+ const server = createServer();
103
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
104
+ const baseUrl = `http://127.0.0.1:${server.address().port}`;
105
+ const execBody = { account_id: "acct_x402", command: "x402 facilitator demo", task_ref: "trust.verify", body: { subject_id: "s-1" } };
106
+
107
+ try {
108
+ // 1. Unpaid -> 402 advertising accepts.
109
+ const unpaid = await post(baseUrl, "/x402/commands/execute", execBody);
110
+ assert.strictEqual(unpaid.status, 402, "unpaid x402 execute must return 402");
111
+ const required = unpaid.json && unpaid.json.payment_required;
112
+ assert.ok(required, "402 must carry payment_required");
113
+ assert.strictEqual(required.x402Version, 2, "challenge must be x402 v2");
114
+ assert.ok(Array.isArray(required.accepts) && required.accepts.length === 1, "challenge must advertise accepts");
115
+ assert.strictEqual(required.accepts[0].network, "eip155:84532", "advertised network mismatch");
116
+ assert.strictEqual(typeof unpaid.headers.get("payment-required"), "string", "PAYMENT-REQUIRED header missing");
117
+
118
+ // 2. Bogus header -> 402 payment_signature_invalid.
119
+ const bogus = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": "not-a-payment" });
120
+ assert.strictEqual(bogus.status, 402, "bogus payment header must 402");
121
+ assert.strictEqual(bogus.json.reason, "payment_signature_invalid", "bogus reason mismatch");
122
+
123
+ // 3. Mismatched requirements -> 402 before the facilitator.
124
+ const verifyBefore = facilitator.calls.verify;
125
+ const mismatch = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("ok", { accepted: { amount: "9" } }) });
126
+ assert.strictEqual(mismatch.status, 402, "mismatched requirements must 402");
127
+ assert.strictEqual(mismatch.json.reason, "payment_requirements_mismatch", "mismatch reason");
128
+ assert.strictEqual(facilitator.calls.verify, verifyBefore, "facilitator must not be called on a requirements mismatch");
129
+
130
+ // 4. Facilitator rejects -> 402 payment_verification_failed.
131
+ const rejected = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("reject-me") });
132
+ assert.strictEqual(rejected.status, 402, "facilitator-rejected payment must 402");
133
+ assert.strictEqual(rejected.json.reason, "payment_verification_failed", "rejection reason");
134
+ assert.strictEqual(rejected.json.detail, "invalid_signature", "rejection detail");
135
+
136
+ // 5. Settlement fails -> 402, no tool output.
137
+ const settleFails = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("settle-fails") });
138
+ assert.strictEqual(settleFails.status, 402, "failed settlement must 402");
139
+ assert.strictEqual(settleFails.json.reason, "payment_settlement_failed", "settlement failure reason");
140
+
141
+ // 6. Verified + settled -> 200 with a result and a PAYMENT-RESPONSE receipt.
142
+ const settleBefore = facilitator.calls.settle;
143
+ const paid = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("ok") });
144
+ assert.strictEqual(paid.status, 200, "verified+settled payment must 200");
145
+ assert.strictEqual(typeof paid.json.transaction.transaction_id, "string", "paid result missing transaction");
146
+ assert.strictEqual(paid.json.transaction.status, "completed", "paid transaction must be completed");
147
+ assert.strictEqual(paid.json.transaction.payment.verification_mode, "x402_facilitator", "verification mode not recorded");
148
+ assert.strictEqual(facilitator.calls.settle, settleBefore + 1, "facilitator settle must be called once");
149
+ const receiptHeader = paid.headers.get("payment-response");
150
+ assert.strictEqual(typeof receiptHeader, "string", "PAYMENT-RESPONSE header missing on a settled 200");
151
+ const receipt = JSON.parse(Buffer.from(receiptHeader, "base64").toString("utf8"));
152
+ assert.strictEqual(receipt.success, true, "settlement receipt must report success");
153
+ assert.strictEqual(receipt.transaction, "0xmocktx", "settlement receipt transaction mismatch");
154
+
155
+ // The facilitator must receive a clean v2 PaymentRequirements: no `resource` field (which
156
+ // strict facilitators like CDP reject), and it must equal the payment payload's `accepted`.
157
+ const verifyBody = facilitator.calls.lastVerifyBody;
158
+ assert.ok(verifyBody && verifyBody.paymentRequirements, "facilitator must receive paymentRequirements");
159
+ assert.strictEqual(verifyBody.paymentRequirements.resource, undefined, "paymentRequirements must not carry a resource field (v2)");
160
+ assert.deepStrictEqual(verifyBody.paymentRequirements, verifyBody.paymentPayload.accepted, "paymentRequirements must equal paymentPayload.accepted");
161
+
162
+ // 7. Native routes must not be disabled by x402_facilitator: a native paid attempt
163
+ // falls back to local_signed semantics, never payment_verification_mode_unsupported.
164
+ const nativeAttempt = await post(baseUrl, "/v1/commands/execute", execBody, {
165
+ "x-wallet-id": "wallet-not-registered",
166
+ "x-payment": "encoded",
167
+ "x-payment-signature": "sig"
168
+ });
169
+ assert.notStrictEqual(nativeAttempt.json && nativeAttempt.json.reason, "payment_verification_mode_unsupported", "native route must fall back to local_signed under x402_facilitator");
170
+
171
+ // 7b. CDP-style facilitator: a non-2xx verify carrying isValid:false is a verification
172
+ // failure, not an unreachable transport error.
173
+ const cdpInvalid = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("cdp-invalid") });
174
+ assert.strictEqual(cdpInvalid.status, 402, "cdp-style 400 verify must still 402");
175
+ assert.strictEqual(cdpInvalid.json.reason, "payment_verification_failed", "cdp-style 400 must map to payment_verification_failed, not facilitator_unreachable");
176
+ assert.strictEqual(cdpInvalid.json.detail, "invalid_payload", "cdp-style verify detail must surface invalidReason");
177
+
178
+ // 7c. CDP-style settle failure: a non-2xx settle carrying success:false is a settlement failure.
179
+ const cdpSettleFail = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("cdp-settle-fail") });
180
+ assert.strictEqual(cdpSettleFail.status, 402, "cdp-style 400 settle must still 402");
181
+ assert.strictEqual(cdpSettleFail.json.reason, "payment_settlement_failed", "cdp-style 400 settle must map to payment_settlement_failed");
182
+
183
+ // 8. Self-serve credits top-up: unpaid -> 402 advertising accepts + credits_per_topup.
184
+ const topupUnpaid = await post(baseUrl, "/x402/credits/topup", { account_id: "acct_topup" });
185
+ assert.strictEqual(topupUnpaid.status, 402, "unpaid topup must 402");
186
+ assert.ok(Array.isArray(topupUnpaid.json.payment_required.accepts), "topup 402 must advertise accepts");
187
+ assert.strictEqual(typeof topupUnpaid.json.payment_required.credits_per_topup, "number", "topup 402 must state credits_per_topup");
188
+
189
+ // 9. Paid top-up -> 200, credits minted to the account, with a settlement receipt.
190
+ const topupPaid = await post(baseUrl, "/x402/credits/topup", { account_id: "acct_topup" }, { "payment-signature": paymentHeader("ok") });
191
+ assert.strictEqual(topupPaid.status, 200, "paid topup must 200");
192
+ assert.strictEqual(topupPaid.json.ok, true, "paid topup must be ok");
193
+ assert.strictEqual(typeof topupPaid.json.credit_grant, "object", "paid topup must return a credit grant");
194
+ const grantedUnits = Number(topupPaid.json.balance.available_units);
195
+ assert.ok(grantedUnits > 0, "paid topup must leave a positive credit balance");
196
+ assert.strictEqual(typeof topupPaid.headers.get("payment-response"), "string", "paid topup must carry a settlement receipt");
197
+
198
+ // 10. Replayed top-up (same payment nonce) is idempotent: no double-mint.
199
+ const topupReplay = await post(baseUrl, "/x402/credits/topup", { account_id: "acct_topup" }, { "payment-signature": paymentHeader("ok") });
200
+ assert.strictEqual(topupReplay.status, 200, "replayed topup must still 200");
201
+ assert.strictEqual(Number(topupReplay.json.balance.available_units), grantedUnits, "replayed topup must not mint credits twice");
202
+
203
+ console.log("xytara verify:x402-facilitator passed");
204
+ } finally {
205
+ await new Promise((resolve) => server.close(resolve));
206
+ await new Promise((resolve) => facilitator.server.close(resolve));
207
+ if (previous.mode === undefined) delete process.env.XYTARA_PAYMENT_VERIFICATION_MODE; else process.env.XYTARA_PAYMENT_VERIFICATION_MODE = previous.mode;
208
+ if (previous.accepts === undefined) delete process.env.XYTARA_X402_ACCEPTS_JSON; else process.env.XYTARA_X402_ACCEPTS_JSON = previous.accepts;
209
+ if (previous.facUrl === undefined) delete process.env.XYTARA_X402_FACILITATOR_URL; else process.env.XYTARA_X402_FACILITATOR_URL = previous.facUrl;
210
+ }
211
+ }
212
+
213
+ main().catch((error) => {
214
+ console.error(error && error.message ? error.message : error);
215
+ process.exitCode = 1;
216
+ });