xytara 3.1.15 → 3.1.17

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,10 +1,4 @@
1
- `xytara` 3.1.15 makes the buyer path safe end to end: a buyer is never charged for a result that was not produced, every failed attempt is visible to the operator, and standard x402 MCP clients can pay the MCP server directly.
1
+ `xytara` 3.1.17 makes the pay-per-call MCP server more tolerant of real-world MCP clients.
2
2
 
3
- - **Pay only for real work** — on a hosted deploy a task is quoted and charged only when it resolves to a real execution target (an operator-pinned target or an active open-network provider). Anything else is refused up front with `501 capability_not_executable` and the list of executable tasks; no payment is requested. The catalog marks every task with `purchase.executable_now` and its per-call price. Post-quantum proof verification (`adapter.xoonya.invoke`) is now a catalog task.
4
- - **Verify, execute, then settle — everywhere** — paid A2A calls now settle only after the skill succeeds, like the HTTP routes. If the facilitator times out at settlement, the runtime checks the EIP-3009 `authorizationState` on-chain: a used authorization is a settled payment and the result is delivered; otherwise the finished result is held and returned when the buyer resends the same payment (bound to the original request). A resent settled payment returns its stored result instead of failing. The execution deadline stays inside the signed authorization's `validBefore`, and the advertised `maxTimeoutSeconds` has a floor of 300.
5
- - **Credits are safe and priced from what was paid** — spending credits requires the account's spend credential; a bare `account_id` is never authority. A self-serve top-up is priced at 100x the per-call amount, is mainnet-only by default, grants credits derived from the amount paid (default $0.0025 per credit), and returns a spend credential to the paying address. Provider revenue on credit spends accrues on the credits' purchase value, never on the catalog quote. Credit routes answer `503` with `Retry-After` while durable state is still being restored.
6
- - **Buyer event log** — every failed buyer attempt is recorded with its stage, reason, fault (ours, upstream, buyer) and a request id the buyer also receives (`X-Request-Id`), with per-day counters for challenges issued and paid calls served. No signatures, request bodies, tokens or full addresses are stored. Operator-only `GET /v1/operator/buyer-events` and `/summary`; the MCP server forwards its events to `POST /v1/operator/buyer-events/ingest`; a daily report job flags money that moved without a delivered result.
7
- - **MCP server** — speaks the x402 MCP transport natively (payment-required tool result; payment in `_meta["x402/payment"]`, `PAYMENT-SIGNATURE` or `X-PAYMENT`; settlement in `_meta["x402/payment-response"]`), with `/mcp/http402` for fetch-wrapper clients. Bad arguments and unknown tools are rejected before any payment, `GET /mcp` is no longer a paid route, and CORS is enabled. Every paid tool was corrected so that an empty, invalid or unverifiable answer is an error and never charged; `json.validate` validates against a JSON Schema subset, `x402.verify-receipt` confirms only a matching USDC transfer, ABI tools are strict, and hostile inputs are bounded.
8
- - **Request-bound agent signatures** — the credit lifecycle routes (reserve, release, reverse) honour the agent's signed binding to the exact account, amount and task.
9
- - **Robustness and privacy** — malformed JSON is a `400` (oversize `413`), internal errors are logged under the request id without leaking details, state-store calls time out, registered providers get a cold-start timeout floor and are woken when a buyer is quoted, and ledger, quote-list, receipt, operations and revenue reads are operator-only on a hosted deploy. x402 challenges show the price actually charged; OpenAPI and AsyncAPI point at the hosted runtime.
3
+ - **Optional methods answered** — `logging/setLevel` is accepted (the server emits no log notifications) and `completion/complete` returns an empty completion, both declared in the server's capabilities, so clients that call them unconditionally no longer get "method not found". Paid tool calls are unchanged.
10
4
  - Published through npm trusted publishing (GitHub OIDC) — no npm token involved.
@@ -16,6 +16,14 @@ const crypto = require("crypto");
16
16
 
17
17
  const DAILY_RETENTION_DAYS = 30;
18
18
 
19
+ // Stages that are normal steps, not failures (the MCP executor sends these with no fault).
20
+ const STEP_STAGES = new Set(["challenge_issued", "paid_ok", "success", "free_ok", "replay_served"]);
21
+
22
+ function isStepEvent(event) {
23
+ const fault = event && event.fault;
24
+ return STEP_STAGES.has(String((event && event.stage) || "")) || fault === null || fault === undefined || fault === "" || fault === "none";
25
+ }
26
+
19
27
  function maxEvents() {
20
28
  const value = Number(process.env.XYTARA_BUYER_EVENT_LOG_MAX || 500);
21
29
  return Number.isFinite(value) && value >= 10 ? Math.min(5000, Math.floor(value)) : 500;
@@ -147,9 +155,11 @@ function recordBuyerEvent(state, input) {
147
155
 
148
156
  function listBuyerEvents(state, query = {}) {
149
157
  ensureBuyerEventCollections(state);
158
+ // (Older entries could be steps mis-filed as failures; never surface them as failures.)
150
159
  const since = query.since && !Number.isNaN(Date.parse(query.since)) ? Date.parse(query.since) : null;
151
160
  const limit = Math.max(1, Math.min(500, Number(query.limit) || 100));
152
161
  const matches = state.buyerEventLog.filter((event) => {
162
+ if (STEP_STAGES.has(String(event.stage || ""))) return false;
153
163
  if (since !== null && Date.parse(event.at_iso) < since) return false;
154
164
  if (query.service && event.service !== query.service) return false;
155
165
  if (query.stage && event.stage !== query.stage) return false;
@@ -163,7 +173,10 @@ function listBuyerEvents(state, query = {}) {
163
173
  function summarizeBuyerEvents(state, days = 7) {
164
174
  ensureBuyerEventCollections(state);
165
175
  const window = Math.max(1, Math.min(DAILY_RETENTION_DAYS, Number(days) || 7));
166
- const keys = Array.from(state.buyerEventDaily.keys()).sort().slice(-window);
176
+ // Calendar window covering at least the last `window` x 24 h: from the UTC day `window` days
177
+ // ago through today (so a report run early in the day still includes yesterday).
178
+ const cutoffDay = new Date(Date.now() - window * 86400000).toISOString().slice(0, 10);
179
+ const keys = Array.from(state.buyerEventDaily.keys()).sort().filter((day) => day >= cutoffDay);
167
180
  const totals = { failures: 0, ours: 0, upstream: 0, buyer: 0, money_moved: 0, challenges_issued: 0, paid_ok: 0 };
168
181
  const byReason = {};
169
182
  const perDay = keys.map((day) => {
@@ -176,6 +189,7 @@ function summarizeBuyerEvents(state, days = 7) {
176
189
  if (rest.startsWith("step:paid_ok")) { dayTotals.paid_ok += count; totals.paid_ok += count; continue; }
177
190
  if (!rest.startsWith("fail:")) continue;
178
191
  const [, stage, reason, fault] = rest.split(":");
192
+ if (STEP_STAGES.has(stage)) continue;
179
193
  dayTotals.failures += count;
180
194
  totals.failures += count;
181
195
  if (fault === "ours") { dayTotals.ours += count; totals.ours += count; }
@@ -206,6 +220,7 @@ function summarizeBuyerEvents(state, days = 7) {
206
220
 
207
221
  module.exports = {
208
222
  countBuyerStep,
223
+ isStepEvent,
209
224
  ensureBuyerEventCollections,
210
225
  listBuyerEvents,
211
226
  payerHash,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xytara",
3
- "version": "3.1.15",
3
+ "version": "3.1.17",
4
4
  "description": "x402 pay-per-call agent-commerce runtime: gasless USDC on Base, an open settling provider network with a ledger-reconciled revenue split and automated on-chain payouts (key-less runtime, delegated signer), and bounded cryptographic delegated spend. Proven live on Base mainnet.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -93,12 +93,15 @@ async function main() {
93
93
  events: [
94
94
  { stage: "challenge_issued", tool: "keccak256" },
95
95
  { stage: "paid_ok", tool: "keccak256" },
96
+ // Normal steps the executor sends with no fault: counted, never failures.
97
+ { stage: "free_ok", reason: "free_tool", fault: null, tool: "catalog" },
98
+ { stage: "replay_served", reason: "settled_result_cache", fault: null, tool: "keccak256" },
96
99
  { stage: "payment_settle", reason: "settle_outcome_unknown", fault: "upstream", tool: "summarize", money_moved: true, http_status: 200 }
97
100
  ]
98
101
  }
99
102
  });
100
103
  assert.strictEqual(ingested.status, 200);
101
- assert.strictEqual(ingested.json.accepted, 3);
104
+ assert.strictEqual(ingested.json.accepted, 5);
102
105
 
103
106
  // 6. Summary: denominators and the attention flag.
104
107
  const summary = await request(baseUrl, "GET", "/v1/operator/buyer-events/summary?days=7", { headers: operator });
@@ -107,6 +110,8 @@ async function main() {
107
110
  assert.ok(summary.json.totals.challenges_issued >= 1 && summary.json.totals.paid_ok >= 1, "steps counted as denominators");
108
111
  assert.strictEqual(summary.json.totals.money_moved, 1, "money moved without delivery is surfaced");
109
112
  assert.strictEqual(summary.json.attention, true, "attention raised");
113
+ assert.strictEqual(summary.json.totals.ours, 0, "normal executor steps are never counted as our-fault failures");
114
+ assert.ok(!summary.json.top_reasons.some((r) => r.reason === "free_tool"), "free_ok is not a failure reason");
110
115
 
111
116
  console.log("xytara verify:buyer-event-log passed");
112
117
  } finally {
package/server.js CHANGED
@@ -17,6 +17,7 @@ const {
17
17
  const {
18
18
  countBuyerStep,
19
19
  ensureBuyerEventCollections,
20
+ isStepEvent,
20
21
  listBuyerEvents,
21
22
  recordBuyerEvent,
22
23
  summarizeBuyerEvents
@@ -4215,8 +4216,10 @@ async function routeRequest(req, res) {
4215
4216
  for (const event of events) {
4216
4217
  if (!event || typeof event !== "object") continue;
4217
4218
  const stage = String(event.stage || "");
4218
- if (stage === "challenge_issued" || stage === "paid_ok" || stage === "success") {
4219
- countBuyerStep(state, stage === "challenge_issued" ? "challenge_issued" : "paid_ok", { service: source, route: event.route || event.tool || "/mcp" });
4219
+ if (isStepEvent(event)) {
4220
+ // A normal step (no fault): counted, never logged as a failure.
4221
+ const step = stage === "challenge_issued" ? "challenge_issued" : (stage === "paid_ok" || stage === "success") ? "paid_ok" : stage || "step";
4222
+ countBuyerStep(state, step, { service: source, route: event.route || event.tool || "/mcp" });
4220
4223
  } else {
4221
4224
  recordBuyerEvent(state, {
4222
4225
  ...event,