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 +2 -8
- package/lib/buyer_event_log.js +16 -1
- package/package.json +1 -1
- package/scripts/verify_buyer_event_log.js +6 -1
- package/server.js +5 -2
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
`xytara` 3.1.
|
|
1
|
+
`xytara` 3.1.17 makes the pay-per-call MCP server more tolerant of real-world MCP clients.
|
|
2
2
|
|
|
3
|
-
- **
|
|
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.
|
package/lib/buyer_event_log.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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,
|
|
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 (
|
|
4219
|
-
|
|
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,
|