xytara 3.1.15 → 3.1.16
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 +3 -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,5 @@
|
|
|
1
|
-
`xytara` 3.1.
|
|
1
|
+
`xytara` 3.1.16 corrects the buyer event log so the operator report only flags real problems.
|
|
2
2
|
|
|
3
|
-
- **
|
|
4
|
-
- **
|
|
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
|
+
- **Normal steps are never failures** — events the MCP server forwards with no fault (a free tool call, a paid call served, a replayed settled result) are counted as steps, not logged as failures; entries already mis-filed are ignored when the log is read.
|
|
4
|
+
- **Calendar report window** — the summary covers at least the requested number of days (from that many UTC days ago through today), so a report run early in the day still includes the previous day.
|
|
10
5
|
- 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.16",
|
|
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,
|