xytara 3.1.1 → 3.1.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.
package/.env.example CHANGED
@@ -1,8 +1,15 @@
1
1
  XYTARA_HOST=127.0.0.1
2
2
  XYTARA_PORT=4320
3
3
  XYTARA_BASE_URL=http://127.0.0.1:4320
4
- # local_signed is the default; set presence only for local demos (accepts any non-empty payment header)
4
+ # local_signed is the default; presence (accepts any non-empty payment header) is for local demos;
5
+ # x402_facilitator is trustless on-chain pay-per-call (see REAL_PAYMENT_SETUP.md and the vars below)
5
6
  XYTARA_PAYMENT_VERIFICATION_MODE=local_signed
7
+ # used only when mode is x402_facilitator:
8
+ XYTARA_X402_ACCEPTS_JSON=[]
9
+ XYTARA_X402_FACILITATOR_URL=https://x402.org/facilitator
10
+ XYTARA_X402_FACILITATOR_HEADERS_JSON={}
11
+ XYTARA_X402_FACILITATOR_TIMEOUT_MS=15000
12
+ XYTARA_X402_SETTLE=true
6
13
  XYTARA_PAYMENT_MAX_AGE_SECONDS=300
7
14
  XYTARA_WALLET_REGISTRY_JSON=[]
8
15
  XYTARA_TREASURY_DESTINATION_ID=
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  `xytara` is the runtime and payment layer for the public two-repo stack.
4
4
 
5
+ > New to the code? Read [`ARCHITECTURE.md`](ARCHITECTURE.md) first. It separates
6
+ > the small execution-and-settlement engine (~9,300 lines) from the large
7
+ > machine-readable breadth catalog, so the real runtime is legible in one sitting.
8
+
5
9
  It exists to let agents, developers, and systems:
6
10
 
7
11
  - discover tasks and workflows
@@ -4,7 +4,7 @@ This guide explains how to move `xytara` from local placeholder payment acceptan
4
4
 
5
5
  ## What Changed
6
6
 
7
- `xytara` supports two payment-acceptance modes. `local_signed` is the default; `presence` has to be opted into explicitly.
7
+ `xytara` supports three payment-acceptance modes. `local_signed` is the default; `presence` and `x402_facilitator` are opt-in.
8
8
 
9
9
  - `presence`
10
10
  - development/demo mode only, enabled with `XYTARA_PAYMENT_VERIFICATION_MODE=presence`
@@ -17,6 +17,34 @@ This guide explains how to move `xytara` from local placeholder payment acceptan
17
17
  - quote replay and nonce replay are blocked
18
18
  - accepted payments are routed into your configured merchant treasury destination posture
19
19
 
20
+ - `x402_facilitator`
21
+ - trustless, on-chain, pay-per-call, the same mechanism proven live on the MCP executor
22
+ - the `/x402/commands/execute` and `/x402/mcp/tools/invoke` routes return a `402` that
23
+ advertises the configured `accepts` (network, asset, amount, payTo) in the
24
+ `PAYMENT-REQUIRED` header; a buyer signs a gasless x402 USDC authorization (EIP-3009)
25
+ - on the paid retry, a facilitator verifies the payment and settles it on-chain before any
26
+ work runs; the response carries a `PAYMENT-RESPONSE` settlement receipt
27
+ - fails closed: if the facilitator is unreachable or the payment does not settle, nothing runs
28
+ - the buyer needs no native gas token; the facilitator broadcasts the transfer
29
+ - enable with `XYTARA_PAYMENT_VERIFICATION_MODE=x402_facilitator` and the x402 env below:
30
+
31
+ ```env
32
+ XYTARA_PAYMENT_VERIFICATION_MODE=x402_facilitator
33
+ # what buyers pay, advertised in the 402 (Base Sepolia test USDC shown):
34
+ XYTARA_X402_ACCEPTS_JSON=[{"scheme":"exact","network":"eip155:84532","asset":"0x036CbD53842c5426634e7929541eC2318f3dCF7e","amount":"1000","payTo":"<your-evm-address>","extra":{"name":"USDC","version":"2"},"maxTimeoutSeconds":60}]
35
+ XYTARA_X402_FACILITATOR_URL=https://x402.org/facilitator
36
+ # optional:
37
+ XYTARA_X402_FACILITATOR_HEADERS_JSON={}
38
+ XYTARA_X402_FACILITATOR_TIMEOUT_MS=15000
39
+ XYTARA_X402_SETTLE=true
40
+ ```
41
+
42
+ Confirm the facilitator settles every network you advertise, and verify the mode end to end
43
+ against a mock facilitator with `npm run verify:x402-facilitator`. The payment a buyer signs
44
+ is identical to the one in `services/mcp-execution-target/buyer-demo/` (an EIP-3009 USDC
45
+ authorization in a `PAYMENT-SIGNATURE` header); only the request body differs, since the main
46
+ runtime takes a command-execute body rather than an MCP JSON-RPC call.
47
+
20
48
  ## Minimum Production-Oriented Configuration
21
49
 
22
50
  Set these environment variables:
package/RELEASE_NOTES.md CHANGED
@@ -1,17 +1,14 @@
1
- `xytara` 3.1.1 is the synchronized security and stability release for the live runtime, payment, execution, delivery, and public first-contact line.
1
+ `xytara` 3.1.2 adds trustless, on-chain, pay-per-call settlement to the main runtime.
2
2
 
3
- This release closes the authorization and payment-verification gaps found in the post-launch review while keeping the public runtime truth models, catalog, first-run path, and naxytra contract surfaces unchanged.
3
+ This release brings the facilitator verify-and-settle mechanism already proven on the public MCP executor to the runtime itself, so every x402 route can settle a real payment on-chain before any work runs, not just the standalone tool server.
4
4
 
5
5
  Highlights:
6
6
 
7
- - operator write surfaces now fail closed: credit minting, allocation reversal, rail-credit application, budgets, agents, economics receipts, job operator actions, participant admission, the operator-shell admin pack, and agent-account issuance all require `Authorization: Bearer` with `XYTARA_ACCOUNT_AUTH_BEARER_TOKEN`, and return `operator_auth_not_configured` instead of opening up when the token is unset
8
- - credit consumption requires either the operator bearer or a valid delegated spend credential
9
- - bearer and credit-bridge token comparisons use constant-time comparison, and the business cockpit no longer accepts query-string tokens
10
- - `XYTARA_PAYMENT_VERIFICATION_MODE` now defaults to `local_signed`; `presence` must be opted into explicitly and logs a startup warning
11
- - the MCP execution target verifies and settles x402 payments through a facilitator before serving paid methods, returns `PAYMENT-RESPONSE` on paid responses, rejects malformed or unadvertised payment payloads, and fails closed when the facilitator is unreachable; `initialize`, `ping`, and `tools/list` stay free so MCP clients can connect
12
- - the credit allocation ledger is no longer truncated, so account balances derived from allocation events stay correct beyond 200 events
13
- - the client binds every paid quote to its account and uses collision-free payment nonces, which strict verification previously rejected as account mismatches or replayed nonces
14
- - `/health` reports restore status honestly and a new `/ready` route returns `503` when runtime state restore or the last snapshot flush failed
15
- - the service verifier now covers unauthenticated mint, wrong operator tokens, credit overdraw, forged and unsigned checkout webhooks, replayed payment nonces, forged payment signatures, unregistered wallets, and query-token rejection; the MCP executor verifier runs against a mock facilitator
16
- - the MCP executor publish workflow skips publishing when the registry already carries the current version
17
- - bundled examples configure a local demo wallet and demo operator tokens explicitly instead of relying on open defaults
7
+ - adds a third payment-acceptance mode, `x402_facilitator`, enabled with `XYTARA_PAYMENT_VERIFICATION_MODE=x402_facilitator`
8
+ - when enabled, `/x402/commands/execute` and `/x402/mcp/tools/invoke` return a `402` that advertises the configured `accepts` (network, asset, amount, payTo) in the `PAYMENT-REQUIRED` header
9
+ - a buyer signs a gasless USDC authorization (EIP-3009); on the paid retry a facilitator verifies and settles it on-chain before any work runs, and the response carries a `PAYMENT-RESPONSE` settlement receipt
10
+ - fails closed: if the facilitator is unreachable or the payment does not settle, nothing runs and no result is returned
11
+ - the mode governs only the x402 routes; native and instant lanes fall back to `local_signed`, so no existing paid path is disabled
12
+ - keeps `local_signed` as the default and `presence` as the local-development opt-in
13
+ - adds `npm run verify:x402-facilitator`, an end-to-end check against a mock facilitator covering the advertised challenge, malformed and mismatched payments, facilitator rejection, failed settlement, and a settled success with receipt
14
+ - documents the mode and its environment in `REAL_PAYMENT_SETUP.md` and the architecture in `ARCHITECTURE.md`
@@ -0,0 +1,30 @@
1
+ {
2
+ "handoff_bundle": {
3
+ "ok": true,
4
+ "handoff_kind": "interaction_result_package",
5
+ "result_package_ref": "result.spine.job.commitment",
6
+ "source_ref": "spine.job.commitment",
7
+ "committed_ref": "transaction.job.commitment",
8
+ "precommit_pack_ref": "precommit.job.commitment.readiness",
9
+ "transaction_id": "txn_7f5d7cd15658",
10
+ "receipt_id": "rcpt_2684569e5241",
11
+ "proof_ref": "proof_rcpt_2684569e5241",
12
+ "payment_protocol": "x402",
13
+ "settlement_mode": "evm_payment",
14
+ "treasury_destination_id": "treasury_acct_demo",
15
+ "task_refs": [
16
+ "jobs.quote",
17
+ "admission.preview",
18
+ "jobs.reserve",
19
+ "jobs.commit"
20
+ ],
21
+ "delivery_count": 1,
22
+ "payment_ledger_count": 1,
23
+ "economic_consequence_handoff_bundle": null,
24
+ "seam_layers": [
25
+ "execution_consequence",
26
+ "payment_settlement_treasury",
27
+ "proof_handoff"
28
+ ]
29
+ }
30
+ }
@@ -311,9 +311,10 @@ async function executeCommandRequest(state, payload, headers, options) {
311
311
  };
312
312
  }
313
313
 
314
- const paymentVerification = verifyPaymentAcceptance(state, headers || {}, requiredHeaders, quote, {
314
+ const paymentVerification = await verifyPaymentAcceptance(state, headers || {}, requiredHeaders, quote, {
315
315
  payment_protocol: opts.paymentProtocol || (payload && payload.payment_protocol) || null,
316
- authorization_scheme: opts.authorizationScheme || null
316
+ authorization_scheme: opts.authorizationScheme || null,
317
+ resource_url: opts.resourceUrl || null
317
318
  });
318
319
  if (!paymentVerification.ok) {
319
320
  return {
@@ -321,6 +322,7 @@ async function executeCommandRequest(state, payload, headers, options) {
321
322
  payload: {
322
323
  ok: false,
323
324
  reason: paymentVerification.reason,
325
+ ...(paymentVerification.detail ? { detail: paymentVerification.detail } : {}),
324
326
  quote
325
327
  }
326
328
  };
@@ -345,7 +347,10 @@ async function executeCommandRequest(state, payload, headers, options) {
345
347
  payload: createCommandResult(state, payload, quote, {
346
348
  paymentVerification,
347
349
  externalExecution: externalExecutionOptions.externalExecution || null
348
- })
350
+ }),
351
+ ...(paymentVerification.payment_response_base64
352
+ ? { paymentResponse: paymentVerification.payment_response_base64 }
353
+ : {})
349
354
  };
350
355
  }
351
356
 
@@ -121,6 +121,7 @@ function loadPaymentConfig() {
121
121
  verification_mode_input: verificationModeInput,
122
122
  payment_max_age_seconds: Number.isFinite(maxAgeSeconds) && maxAgeSeconds > 0 ? maxAgeSeconds : 300,
123
123
  wallet_registry: loadWalletRegistry(),
124
+ x402: loadX402FacilitatorConfig(),
124
125
  treasury_destination_id: String(process.env.XYTARA_TREASURY_DESTINATION_ID || "").trim() || null,
125
126
  treasury_destination_kind: String(process.env.XYTARA_TREASURY_DESTINATION_KIND || "").trim() || null,
126
127
  treasury_account_id: String(process.env.XYTARA_TREASURY_ACCOUNT_ID || "").trim() || null,
@@ -129,6 +130,51 @@ function loadPaymentConfig() {
129
130
  };
130
131
  }
131
132
 
133
+ // Configuration for the x402_facilitator verification mode: the on-chain, trustless
134
+ // pay-per-call path. The advertised accepts tell a buyer how to pay; the facilitator
135
+ // verifies and settles the signed payment before any work runs.
136
+ function loadX402FacilitatorConfig() {
137
+ let accepts = [];
138
+ const rawAccepts = String(process.env.XYTARA_X402_ACCEPTS_JSON || "").trim();
139
+ if (rawAccepts) {
140
+ try {
141
+ const parsed = JSON.parse(rawAccepts);
142
+ if (Array.isArray(parsed)) {
143
+ accepts = parsed
144
+ .map((entry) => ({
145
+ scheme: String((entry && entry.scheme) || "exact").trim(),
146
+ network: String((entry && entry.network) || "").trim(),
147
+ asset: String((entry && entry.asset) || "").trim(),
148
+ amount: String((entry && entry.amount) || "").trim(),
149
+ payTo: String((entry && entry.payTo) || "").trim(),
150
+ ...(entry && entry.extra && typeof entry.extra === "object" ? { extra: entry.extra } : {}),
151
+ ...(Number(entry && entry.maxTimeoutSeconds) > 0 ? { maxTimeoutSeconds: Number(entry.maxTimeoutSeconds) } : {})
152
+ }))
153
+ .filter((entry) => entry.network && entry.asset && entry.amount && entry.payTo);
154
+ }
155
+ } catch (_) {
156
+ accepts = [];
157
+ }
158
+ }
159
+ let headers = {};
160
+ const rawHeaders = String(process.env.XYTARA_X402_FACILITATOR_HEADERS_JSON || "").trim();
161
+ if (rawHeaders) {
162
+ try {
163
+ const parsed = JSON.parse(rawHeaders);
164
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) headers = parsed;
165
+ } catch (_) {
166
+ headers = {};
167
+ }
168
+ }
169
+ return {
170
+ accepts,
171
+ facilitator_url: String(process.env.XYTARA_X402_FACILITATOR_URL || "https://x402.org/facilitator").trim().replace(/\/$/, ""),
172
+ facilitator_headers: headers,
173
+ facilitator_timeout_ms: Math.max(1000, Number(process.env.XYTARA_X402_FACILITATOR_TIMEOUT_MS || 15000) || 15000),
174
+ settle: String(process.env.XYTARA_X402_SETTLE || "true").trim().toLowerCase() !== "false"
175
+ };
176
+ }
177
+
132
178
  function buildConfiguredTreasuryDestination(accountId, settlementMode) {
133
179
  const config = loadPaymentConfig();
134
180
  const normalizedSettlementMode = normalizeSettlementMode(settlementMode);
@@ -2,6 +2,12 @@
2
2
 
3
3
  const crypto = require("crypto");
4
4
  const { loadPaymentConfig } = require("./payment_config");
5
+ const {
6
+ createFacilitatorClient,
7
+ verifyX402Payment,
8
+ settleX402Payment,
9
+ encodePaymentResponse
10
+ } = require("./x402_facilitator");
5
11
  const { parseL402Authorization, verifyL402AccessToken } = require("./l402_payment_front");
6
12
  const { parseBoltAuthorization, verifyBoltAccessToken } = require("./bolt_payment_front");
7
13
  const { parseStripeMppAuthorization, verifyStripeMppAccessToken } = require("./stripe_mpp_payment_front");
@@ -157,10 +163,48 @@ function verifyPaymentPayloadAgainstQuote(paymentPayload, quote, walletId, maxAg
157
163
  return { ok: true, age_seconds: ageSeconds };
158
164
  }
159
165
 
160
- function verifyPaymentAcceptance(state, headers, requiredHeaders, quote, options) {
166
+ async function verifyPaymentAcceptance(state, headers, requiredHeaders, quote, options) {
161
167
  const config = loadPaymentConfig();
162
168
  const required = Array.isArray(requiredHeaders) ? requiredHeaders : [];
163
169
  const opts = options || {};
170
+ // Trustless on-chain pay-per-call: the PAYMENT-SIGNATURE carries an x402 v2 payment,
171
+ // which a facilitator verifies and settles on-chain before any work runs. This path is
172
+ // opt-in (XYTARA_PAYMENT_VERIFICATION_MODE=x402_facilitator) and fails closed if the
173
+ // facilitator is unreachable or the payment does not settle.
174
+ if (config.verification_mode === "x402_facilitator" && required.includes("payment-signature")) {
175
+ const h = headers || {};
176
+ const facilitator = createFacilitatorClient({
177
+ url: config.x402.facilitator_url,
178
+ headers: config.x402.facilitator_headers,
179
+ timeoutMs: config.x402.facilitator_timeout_ms
180
+ });
181
+ const auth = await verifyX402Payment({
182
+ paymentHeader: h["payment-signature"],
183
+ accepts: config.x402.accepts,
184
+ resourceUrl: opts.resource_url || null,
185
+ facilitator
186
+ });
187
+ if (!auth.ok) {
188
+ return { ok: false, status: auth.status || 402, reason: auth.reason, detail: auth.detail || null };
189
+ }
190
+ const settled = await settleX402Payment({ authorization: auth.authorization, facilitator, settle: config.x402.settle });
191
+ if (!settled.ok) {
192
+ return { ok: false, status: settled.status || 402, reason: settled.reason, detail: settled.detail || null };
193
+ }
194
+ return {
195
+ ok: true,
196
+ verification_mode: "x402_facilitator",
197
+ wallet_id: auth.authorization.payer,
198
+ payment_payload: {
199
+ x402_payer: auth.authorization.payer,
200
+ x402_network: auth.authorization.paymentRequirements.network,
201
+ x402_asset: auth.authorization.paymentRequirements.asset,
202
+ x402_amount: auth.authorization.paymentRequirements.amount
203
+ },
204
+ settlement: settled.settlement,
205
+ payment_response_base64: encodePaymentResponse(settled.settlement)
206
+ };
207
+ }
164
208
  if (required.includes("authorization")) {
165
209
  if (opts.authorization_scheme === "mpp" || opts.payment_protocol === "mpp") {
166
210
  return verifyStripeMppAccessToken(headers || {}, quote);
@@ -171,7 +215,11 @@ function verifyPaymentAcceptance(state, headers, requiredHeaders, quote, options
171
215
  return verifyL402AccessToken(headers || {}, quote);
172
216
  }
173
217
  const payment = parsePayment(headers || {}, requiredHeaders, opts);
174
- if (config.verification_mode === "presence") {
218
+ // x402_facilitator only governs the x402 (payment-signature) routes, handled above.
219
+ // On the native and instant routes it falls back to local_signed, so enabling the mode
220
+ // adds trustless on-chain settlement without disabling the existing paid lanes.
221
+ const nativeMode = config.verification_mode === "x402_facilitator" ? "local_signed" : config.verification_mode;
222
+ if (nativeMode === "presence") {
175
223
  return {
176
224
  ok: true,
177
225
  verification_mode: "presence",
@@ -180,7 +228,7 @@ function verifyPaymentAcceptance(state, headers, requiredHeaders, quote, options
180
228
  };
181
229
  }
182
230
 
183
- if (config.verification_mode !== "local_signed") {
231
+ if (nativeMode !== "local_signed") {
184
232
  return {
185
233
  ok: false,
186
234
  status: 400,
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+
3
+ // Self-contained x402 v2 facilitator client for the main runtime.
4
+ //
5
+ // This mirrors the proven logic in services/mcp-execution-target/server.js but takes
6
+ // all configuration as explicit arguments so it has no dependency on any global state
7
+ // and can be reused by any paid route. It is intentionally duplicated rather than shared
8
+ // with the standalone MCP executor, which deploys from its own subdirectory.
9
+ //
10
+ // The trust model is the same one proven live: this process never verifies balances or
11
+ // broadcasts transfers itself. It asks an x402 facilitator to verify the buyer's signed
12
+ // EIP-3009 authorization and to settle it on-chain, and refuses to serve work unless the
13
+ // facilitator confirms both. If the facilitator is unreachable, access fails closed.
14
+
15
+ function normalizeString(value) {
16
+ return typeof value === "string" ? value.trim() : "";
17
+ }
18
+
19
+ function normalizeAddress(value) {
20
+ return normalizeString(value).toLowerCase();
21
+ }
22
+
23
+ // Decode a PAYMENT-SIGNATURE header into an x402 v2 payment payload.
24
+ function decodePaymentSignatureHeader(value) {
25
+ const raw = normalizeString(value);
26
+ if (!raw) return null;
27
+ for (const encoding of ["base64url", "base64"]) {
28
+ try {
29
+ const decoded = Buffer.from(raw, encoding).toString("utf8");
30
+ const parsed = JSON.parse(decoded);
31
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
32
+ if (Number(parsed.x402Version) !== 2) continue;
33
+ if (!parsed.accepted || typeof parsed.accepted !== "object") continue;
34
+ if (!parsed.payload || typeof parsed.payload !== "object") continue;
35
+ return parsed;
36
+ } catch (_) {
37
+ // try the next encoding
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+
43
+ // Find the advertised requirements entry the buyer claims to be paying.
44
+ function matchAcceptedRequirements(paymentPayload, accepts) {
45
+ const accepted = paymentPayload && paymentPayload.accepted ? paymentPayload.accepted : null;
46
+ if (!accepted || !Array.isArray(accepts)) return null;
47
+ return (
48
+ accepts.find(
49
+ (entry) =>
50
+ entry.scheme === normalizeString(accepted.scheme) &&
51
+ entry.network === normalizeString(accepted.network) &&
52
+ normalizeAddress(entry.asset) === normalizeAddress(accepted.asset) &&
53
+ normalizeAddress(entry.payTo) === normalizeAddress(accepted.payTo) &&
54
+ String(entry.amount) === String(accepted.amount)
55
+ ) || null
56
+ );
57
+ }
58
+
59
+ function createFacilitatorClient(config) {
60
+ const url = normalizeString(config && config.url).replace(/\/$/, "");
61
+ const headers = config && config.headers && typeof config.headers === "object" ? config.headers : {};
62
+ const timeoutMs = Math.max(1000, Number(config && config.timeoutMs) || 15000);
63
+
64
+ async function post(path, body) {
65
+ const controller = new AbortController();
66
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
67
+ try {
68
+ const response = await fetch(`${url}${path}`, {
69
+ method: "POST",
70
+ headers: { "content-type": "application/json", accept: "application/json", ...headers },
71
+ body: JSON.stringify(body),
72
+ signal: controller.signal
73
+ });
74
+ const text = await response.text();
75
+ let json = null;
76
+ try {
77
+ json = text ? JSON.parse(text) : null;
78
+ } catch (_) {
79
+ json = null;
80
+ }
81
+ return { ok: response.ok, status: response.status, json };
82
+ } catch (error) {
83
+ return { ok: false, status: 0, json: null, error: error && error.message ? error.message : "facilitator_request_failed" };
84
+ } finally {
85
+ clearTimeout(timer);
86
+ }
87
+ }
88
+
89
+ return {
90
+ verify: (payload, requirements) => post("/verify", { x402Version: 2, paymentPayload: payload, paymentRequirements: requirements }),
91
+ settle: (payload, requirements) => post("/settle", { x402Version: 2, paymentPayload: payload, paymentRequirements: requirements })
92
+ };
93
+ }
94
+
95
+ // Verify a buyer's x402 payment against the advertised requirements via the facilitator.
96
+ // Returns { ok:true, authorization } or { ok:false, status:402, reason, detail }.
97
+ async function verifyX402Payment({ paymentHeader, accepts, resourceUrl, facilitator }) {
98
+ const header = normalizeString(paymentHeader);
99
+ if (!header) {
100
+ return { ok: false, status: 402, reason: "payment_required" };
101
+ }
102
+ const paymentPayload = decodePaymentSignatureHeader(header);
103
+ if (!paymentPayload) {
104
+ return { ok: false, status: 402, reason: "payment_signature_invalid", detail: "PAYMENT-SIGNATURE must be a base64 JSON x402 v2 payment payload" };
105
+ }
106
+ const paymentRequirements = matchAcceptedRequirements(paymentPayload, accepts);
107
+ if (!paymentRequirements) {
108
+ return { ok: false, status: 402, reason: "payment_requirements_mismatch", detail: "accepted requirements do not match any advertised payment option" };
109
+ }
110
+ const requirementsForFacilitator = { ...paymentRequirements, resource: resourceUrl };
111
+ const verification = await facilitator.verify(paymentPayload, requirementsForFacilitator);
112
+ if (!verification.ok || !verification.json) {
113
+ return { ok: false, status: 402, reason: "facilitator_unreachable", detail: verification.error || `facilitator verify returned HTTP ${verification.status}` };
114
+ }
115
+ if (verification.json.isValid !== true) {
116
+ return { ok: false, status: 402, reason: "payment_verification_failed", detail: normalizeString(verification.json.invalidReason) || "facilitator rejected the payment" };
117
+ }
118
+ return {
119
+ ok: true,
120
+ authorization: {
121
+ paymentPayload,
122
+ paymentRequirements: requirementsForFacilitator,
123
+ payer: normalizeString(verification.json.payer) || null
124
+ }
125
+ };
126
+ }
127
+
128
+ // Settle a verified payment on-chain via the facilitator (unless settlement is disabled).
129
+ // Returns { ok:true, settlement } or { ok:false, status:402, reason, detail }.
130
+ async function settleX402Payment({ authorization, facilitator, settle }) {
131
+ if (settle === false) {
132
+ return { ok: true, settlement: { success: true, skipped: true, reason: "settlement_disabled_by_operator", payer: authorization.payer } };
133
+ }
134
+ const settlement = await facilitator.settle(authorization.paymentPayload, authorization.paymentRequirements);
135
+ if (!settlement.ok || !settlement.json) {
136
+ return { ok: false, status: 402, reason: "facilitator_unreachable", detail: settlement.error || `facilitator settle returned HTTP ${settlement.status}` };
137
+ }
138
+ if (settlement.json.success !== true) {
139
+ return { ok: false, status: 402, reason: "payment_settlement_failed", detail: normalizeString(settlement.json.errorReason) || "facilitator could not settle the payment" };
140
+ }
141
+ return { ok: true, settlement: settlement.json };
142
+ }
143
+
144
+ function encodePaymentResponse(settlement) {
145
+ return Buffer.from(JSON.stringify(settlement), "utf8").toString("base64");
146
+ }
147
+
148
+ module.exports = {
149
+ decodePaymentSignatureHeader,
150
+ matchAcceptedRequirements,
151
+ createFacilitatorClient,
152
+ verifyX402Payment,
153
+ settleX402Payment,
154
+ encodePaymentResponse
155
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xytara",
3
- "version": "3.1.1",
3
+ "version": "3.1.2",
4
4
  "description": "Agent-commerce runtime for quote, pay, execute, deliver, meter, and integrate.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -99,7 +99,8 @@
99
99
  "verify:release-candidate": "node scripts/verify_release_candidate.js",
100
100
  "verify:production-readiness": "node scripts/verify_production_readiness.js",
101
101
  "verify:final-activation": "node scripts/verify_final_activation.js",
102
- "verify:all": "node scripts/verify_all.js && node scripts/verify_integrations.js && node scripts/verify_adapters.js && node scripts/verify_control_plane_contracts.js && node scripts/verify_control_plane_routes.js && node scripts/verify_framework_provider_promotion.js && node scripts/verify_operator_observability_boundary.js && node scripts/verify_pricing_experiment_plan.js && node scripts/verify_treasury_public_boundary.js && node scripts/verify_market_lanes.js && node scripts/verify_market_lane_depth.js && node scripts/verify_market_lane_traces.js && node scripts/verify_market_lane_promotion.js && node scripts/verify_provider_runtime_breadth.js && node scripts/verify_provider_runtime_journeys.js && node scripts/verify_lifecycle_continuity.js && node scripts/verify_first_run_conversions.js && node scripts/verify_delivery_summary.js && node scripts/verify_runtime_durability.js && node scripts/verify_revenue_launch_paths.js && node scripts/verify_full_catalog.js && node scripts/verify_tooling.js && node scripts/verify_examples.js && node scripts/verify_service.js",
102
+ "verify:x402-facilitator": "node scripts/verify_x402_facilitator.js",
103
+ "verify:all": "node scripts/verify_all.js && node scripts/verify_integrations.js && node scripts/verify_adapters.js && node scripts/verify_control_plane_contracts.js && node scripts/verify_control_plane_routes.js && node scripts/verify_framework_provider_promotion.js && node scripts/verify_operator_observability_boundary.js && node scripts/verify_pricing_experiment_plan.js && node scripts/verify_treasury_public_boundary.js && node scripts/verify_market_lanes.js && node scripts/verify_market_lane_depth.js && node scripts/verify_market_lane_traces.js && node scripts/verify_market_lane_promotion.js && node scripts/verify_provider_runtime_breadth.js && node scripts/verify_provider_runtime_journeys.js && node scripts/verify_lifecycle_continuity.js && node scripts/verify_first_run_conversions.js && node scripts/verify_delivery_summary.js && node scripts/verify_runtime_durability.js && node scripts/verify_revenue_launch_paths.js && node scripts/verify_full_catalog.js && node scripts/verify_tooling.js && node scripts/verify_examples.js && node scripts/verify_service.js && node scripts/verify_x402_facilitator.js",
103
104
  "prepublishOnly": "npm run verify:release-candidate"
104
105
  }
105
106
  }
@@ -0,0 +1,174 @@
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
+ console.log("xytara verify:x402-facilitator passed");
162
+ } finally {
163
+ await new Promise((resolve) => server.close(resolve));
164
+ await new Promise((resolve) => facilitator.server.close(resolve));
165
+ if (previous.mode === undefined) delete process.env.XYTARA_PAYMENT_VERIFICATION_MODE; else process.env.XYTARA_PAYMENT_VERIFICATION_MODE = previous.mode;
166
+ if (previous.accepts === undefined) delete process.env.XYTARA_X402_ACCEPTS_JSON; else process.env.XYTARA_X402_ACCEPTS_JSON = previous.accepts;
167
+ if (previous.facUrl === undefined) delete process.env.XYTARA_X402_FACILITATOR_URL; else process.env.XYTARA_X402_FACILITATOR_URL = previous.facUrl;
168
+ }
169
+ }
170
+
171
+ main().catch((error) => {
172
+ console.error(error && error.message ? error.message : error);
173
+ process.exitCode = 1;
174
+ });
package/server.js CHANGED
@@ -49,6 +49,28 @@ function buildPaymentRequiredHeaders(paymentRequiredPayload) {
49
49
  "Cache-Control": "no-store"
50
50
  };
51
51
  }
52
+
53
+ // The public resource URL of the current request, used as the x402 payment resource.
54
+ function buildRequestResourceUrl(req, url) {
55
+ const headers = (req && req.headers) || {};
56
+ const forwardedProto = String(headers["x-forwarded-proto"] || "").split(",")[0].trim();
57
+ const protocol = forwardedProto || (process.env.RENDER ? "https" : "http");
58
+ const host = String(headers["x-forwarded-host"] || headers.host || "").split(",")[0].trim();
59
+ if (!host) return null;
60
+ return `${protocol}://${host}${url.pathname}`;
61
+ }
62
+
63
+ // Response headers for an x402 route: PAYMENT-REQUIRED on a 402 challenge, or
64
+ // PAYMENT-RESPONSE carrying the settlement receipt on a settled 200.
65
+ function buildX402ResponseHeaders(result) {
66
+ if (result && result.status === 402 && result.payload && result.payload.payment_required) {
67
+ return buildPaymentRequiredHeaders(result.payload.payment_required);
68
+ }
69
+ if (result && result.status === 200 && result.paymentResponse) {
70
+ return { "PAYMENT-RESPONSE": result.paymentResponse, "Cache-Control": "no-store" };
71
+ }
72
+ return undefined;
73
+ }
52
74
  const {
53
75
  completePendingExecution,
54
76
  createCommandResult,
@@ -8898,24 +8920,21 @@ async function routeRequest(req, res) {
8898
8920
  ...(body && typeof body === "object" && !Array.isArray(body) ? body : {}),
8899
8921
  payment_protocol: "x402"
8900
8922
  };
8923
+ const x402Accepts = loadPaymentConfig().x402.accepts;
8901
8924
  const result = await executeCommandRequest(state, payload, req.headers, {
8902
8925
  requiredHeaders: ["payment-signature"],
8926
+ resourceUrl: buildRequestResourceUrl(req, url),
8903
8927
  challengePayload: (quote) => ({
8904
8928
  ok: false,
8905
8929
  payment_required: {
8930
+ x402Version: 2,
8906
8931
  scheme: "exact",
8907
- quote
8932
+ quote,
8933
+ ...(x402Accepts.length > 0 ? { accepts: x402Accepts } : {})
8908
8934
  }
8909
8935
  })
8910
8936
  });
8911
- sendJson(
8912
- res,
8913
- result.status,
8914
- result.payload,
8915
- result.status === 402 && result.payload && result.payload.payment_required
8916
- ? buildPaymentRequiredHeaders(result.payload.payment_required)
8917
- : undefined
8918
- );
8937
+ sendJson(res, result.status, result.payload, buildX402ResponseHeaders(result));
8919
8938
  return;
8920
8939
  }
8921
8940
 
@@ -10566,24 +10585,21 @@ async function routeRequest(req, res) {
10566
10585
  if (req.method === "POST" && url.pathname === "/x402/mcp/tools/invoke") {
10567
10586
  const body = await readJsonBody(req);
10568
10587
  const payload = buildMcpInvokePayload(body, { paymentProtocol: "x402" });
10588
+ const x402Accepts = loadPaymentConfig().x402.accepts;
10569
10589
  const result = await executeCommandRequest(state, payload, req.headers, {
10570
10590
  requiredHeaders: ["payment-signature"],
10591
+ resourceUrl: buildRequestResourceUrl(req, url),
10571
10592
  challengePayload: (quote) => ({
10572
10593
  ok: false,
10573
10594
  payment_required: {
10595
+ x402Version: 2,
10574
10596
  scheme: "exact",
10575
- quote
10597
+ quote,
10598
+ ...(x402Accepts.length > 0 ? { accepts: x402Accepts } : {})
10576
10599
  }
10577
10600
  })
10578
10601
  });
10579
- sendJson(
10580
- res,
10581
- result.status,
10582
- result.payload,
10583
- result.status === 402 && result.payload && result.payload.payment_required
10584
- ? buildPaymentRequiredHeaders(result.payload.payment_required)
10585
- : undefined
10586
- );
10602
+ sendJson(res, result.status, result.payload, buildX402ResponseHeaders(result));
10587
10603
  return;
10588
10604
  }
10589
10605