@zeroxyz/cli 0.0.36 → 0.0.38
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/README.md +2 -2
- package/dist/index.js +433 -135
- package/package.json +3 -3
- package/skills/zero/SKILL.md +9 -2
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Search for capabilities by free-text query.
|
|
|
46
46
|
zero search "image classification"
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Results are numbered. Use `zero get <number>` to view details.
|
|
49
|
+
Results are numbered and include public rating/review context. Use `zero get <number>` to view details.
|
|
50
50
|
|
|
51
51
|
**Cost filtering.** By default, results are filtered to capabilities priced **≤ $30/call** as a wallet-safety cap. Override per-call:
|
|
52
52
|
|
|
@@ -56,7 +56,7 @@ zero search "expensive deep research" --max-cost 100 # raise the cap for hard ta
|
|
|
56
56
|
zero search "image classification" --free # only free capabilities
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
Other useful filters: `--
|
|
59
|
+
Other useful filters: `--protocol x402|mpp`, `--status healthy|degraded|down`, `--source <name>`, `--all` (disables default quality filtering).
|
|
60
60
|
|
|
61
61
|
### `zero get <position>`
|
|
62
62
|
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { join as join8 } from "path";
|
|
|
7
7
|
// package.json
|
|
8
8
|
var package_default = {
|
|
9
9
|
name: "@zeroxyz/cli",
|
|
10
|
-
version: "0.0.
|
|
10
|
+
version: "0.0.38",
|
|
11
11
|
type: "module",
|
|
12
12
|
bin: {
|
|
13
13
|
zero: "dist/index.js",
|
|
@@ -23,7 +23,7 @@ var package_default = {
|
|
|
23
23
|
},
|
|
24
24
|
scripts: {
|
|
25
25
|
build: "tsup src/index.ts --format esm --out-dir dist --clean",
|
|
26
|
-
"build:binary": "tsup --config tsup.binary.ts && cp -r skills hooks dist/pkg/ && pnpm exec pkg dist/pkg/index.cjs --config pkg.json --targets node24-macos-arm64,node24-macos-x64,node24-linux-x64 --output dist/bin/zero",
|
|
26
|
+
"build:binary": "tsup --config tsup.binary.ts && cp -r skills hooks dist/pkg/ && pnpm exec pkg dist/pkg/index.cjs --config pkg.json --targets node24-macos-arm64,node24-macos-x64,node24-linux-x64,node24-linux-arm64 --output dist/bin/zero",
|
|
27
27
|
prepublishOnly: "pnpm run build",
|
|
28
28
|
dev: "ZERO_ENV=development tsx src/index.ts",
|
|
29
29
|
cli: "ZERO_ENV=development ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
|
|
@@ -41,7 +41,7 @@ var package_default = {
|
|
|
41
41
|
"@x402/extensions": "^2.9.0",
|
|
42
42
|
"@x402/fetch": "^2.9.0",
|
|
43
43
|
commander: "^13.0.0",
|
|
44
|
-
mppx: "^0.
|
|
44
|
+
mppx: "^0.6.9",
|
|
45
45
|
open: "^11.0.0",
|
|
46
46
|
"posthog-node": "^5.29.2",
|
|
47
47
|
viem: "^2.47.10",
|
|
@@ -72,6 +72,13 @@ import { z as z2 } from "zod";
|
|
|
72
72
|
// src/services/api-service.ts
|
|
73
73
|
import { createHash } from "crypto";
|
|
74
74
|
import z from "zod";
|
|
75
|
+
var ratingSchema = z.object({
|
|
76
|
+
score: z.string(),
|
|
77
|
+
successRate: z.string(),
|
|
78
|
+
reviews: z.number(),
|
|
79
|
+
stars: z.string().nullable().optional(),
|
|
80
|
+
state: z.enum(["unrated", "rated"]).optional()
|
|
81
|
+
});
|
|
75
82
|
var searchResultSchema = z.object({
|
|
76
83
|
id: z.string(),
|
|
77
84
|
position: z.number(),
|
|
@@ -83,17 +90,10 @@ var searchResultSchema = z.object({
|
|
|
83
90
|
url: z.string(),
|
|
84
91
|
urlTemplate: z.string().nullable().optional(),
|
|
85
92
|
cost: z.object({ amount: z.string(), asset: z.string() }),
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
successRate: z.string(),
|
|
89
|
-
reviews: z.number(),
|
|
90
|
-
stars: z.string().nullable().optional(),
|
|
91
|
-
state: z.enum(["unrated", "rated"]).optional()
|
|
92
|
-
}),
|
|
93
|
-
trustScore: z.number().nullable().optional(),
|
|
94
|
-
trustSignalCount: z.number().optional(),
|
|
93
|
+
reviewCount: z.number().optional(),
|
|
94
|
+
rating: ratingSchema,
|
|
95
95
|
availabilityStatus: z.enum(["healthy", "degraded", "down", "unknown"]).nullable().optional(),
|
|
96
|
-
|
|
96
|
+
displayStatus: z.enum(["healthy", "stable", "degraded", "unhealthy", "unknown"]).optional()
|
|
97
97
|
});
|
|
98
98
|
var searchResponseSchema = z.object({
|
|
99
99
|
searchId: z.string(),
|
|
@@ -115,15 +115,11 @@ var capabilityResponseSchema = z.object({
|
|
|
115
115
|
responseSchema: z.record(z.string(), z.unknown()).nullable(),
|
|
116
116
|
example: z.object({ request: z.unknown(), response: z.unknown() }).nullable(),
|
|
117
117
|
tags: z.array(z.string()).nullable(),
|
|
118
|
+
exampleAgentPrompt: z.string().nullable().optional(),
|
|
118
119
|
displayCostAmount: z.string(),
|
|
119
120
|
displayCostAsset: z.string(),
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
successRate: z.string(),
|
|
123
|
-
reviews: z.number(),
|
|
124
|
-
stars: z.string().nullable().optional(),
|
|
125
|
-
state: z.enum(["unrated", "rated"]).optional()
|
|
126
|
-
}),
|
|
121
|
+
reviewCount: z.number(),
|
|
122
|
+
rating: ratingSchema,
|
|
127
123
|
priceObserved: z.object({
|
|
128
124
|
minCents: z.string().nullable(),
|
|
129
125
|
medianCents: z.string().nullable(),
|
|
@@ -146,13 +142,8 @@ var capabilityResponseSchema = z.object({
|
|
|
146
142
|
priority: z.number()
|
|
147
143
|
})
|
|
148
144
|
).nullable(),
|
|
149
|
-
trustScore: z.number().nullable().optional(),
|
|
150
|
-
trustComponents: z.object({
|
|
151
|
-
apiQuality: z.number().nullable(),
|
|
152
|
-
blockchainActivity: z.number().nullable(),
|
|
153
|
-
performance: z.number().nullable()
|
|
154
|
-
}).nullable().optional(),
|
|
155
145
|
availabilityStatus: z.enum(["healthy", "degraded", "down", "unknown"]).nullable().optional(),
|
|
146
|
+
displayStatus: z.enum(["healthy", "stable", "degraded", "unhealthy", "unknown"]).optional(),
|
|
156
147
|
activationCount: z.number().optional(),
|
|
157
148
|
lastUsedAt: z.string().nullable().optional(),
|
|
158
149
|
lastSuccessfullyRanAt: z.string().nullable().optional()
|
|
@@ -238,7 +229,7 @@ var buildCanonicalMessage = (method, path, body, timestamp, nonce) => {
|
|
|
238
229
|
const bodyHash = createHash("sha256").update(body ?? "").digest("hex");
|
|
239
230
|
return `${method}:${path}:${bodyHash}:${timestamp}:${nonce}`;
|
|
240
231
|
};
|
|
241
|
-
var ApiService = class {
|
|
232
|
+
var ApiService = class _ApiService {
|
|
242
233
|
constructor(baseUrl, account) {
|
|
243
234
|
this.baseUrl = baseUrl;
|
|
244
235
|
this.account = account;
|
|
@@ -246,6 +237,7 @@ var ApiService = class {
|
|
|
246
237
|
}
|
|
247
238
|
walletAddress;
|
|
248
239
|
account;
|
|
240
|
+
withAccount = (account) => new _ApiService(this.baseUrl, account);
|
|
249
241
|
signRequest = async (method, path, body) => {
|
|
250
242
|
if (!this.account) throw new Error("No private key configured");
|
|
251
243
|
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
@@ -1130,8 +1122,9 @@ var PaymentService = class {
|
|
|
1130
1122
|
return { viemChain: tempoTestnetChain, token: PATHUSD_TEMPO };
|
|
1131
1123
|
}
|
|
1132
1124
|
};
|
|
1133
|
-
getBalanceRaw = async (chain) => {
|
|
1134
|
-
|
|
1125
|
+
getBalanceRaw = async (chain, address) => {
|
|
1126
|
+
const target = address ?? this.account?.address;
|
|
1127
|
+
if (!target) return 0n;
|
|
1135
1128
|
const { viemChain, token } = this.resolveChainConfig(chain);
|
|
1136
1129
|
const client = createPublicClient({
|
|
1137
1130
|
chain: viemChain,
|
|
@@ -1141,7 +1134,7 @@ var PaymentService = class {
|
|
|
1141
1134
|
address: token,
|
|
1142
1135
|
abi: ERC20_BALANCE_ABI,
|
|
1143
1136
|
functionName: "balanceOf",
|
|
1144
|
-
args: [
|
|
1137
|
+
args: [target]
|
|
1145
1138
|
});
|
|
1146
1139
|
return balance;
|
|
1147
1140
|
};
|
|
@@ -1161,6 +1154,13 @@ var PaymentService = class {
|
|
|
1161
1154
|
]);
|
|
1162
1155
|
return { amount: formatUnits(baseRaw + tempoRaw, 6), asset: "USDC" };
|
|
1163
1156
|
};
|
|
1157
|
+
getTotalBalanceForAddress = async (address) => {
|
|
1158
|
+
const [baseRaw, tempoRaw] = await Promise.all([
|
|
1159
|
+
this.getBalanceRaw("base", address),
|
|
1160
|
+
this.getBalanceRaw("tempo", address)
|
|
1161
|
+
]);
|
|
1162
|
+
return { amount: formatUnits(baseRaw + tempoRaw, 6), asset: "USDC" };
|
|
1163
|
+
};
|
|
1164
1164
|
};
|
|
1165
1165
|
|
|
1166
1166
|
// src/util/infer-schema.ts
|
|
@@ -1247,30 +1247,69 @@ var isJsonContentType = (contentType) => {
|
|
|
1247
1247
|
const ct = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
|
|
1248
1248
|
return ct === "application/json" || ct.endsWith("+json");
|
|
1249
1249
|
};
|
|
1250
|
-
var
|
|
1251
|
-
var
|
|
1250
|
+
var MAX_INLINE_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
|
|
1251
|
+
var MAX_FILE_REQUEST_BODY_BYTES = 500 * 1024 * 1024;
|
|
1252
|
+
var UPSTREAM_ERROR_FIELDS = [
|
|
1253
|
+
"error",
|
|
1254
|
+
"message",
|
|
1255
|
+
"detail",
|
|
1256
|
+
"reason",
|
|
1257
|
+
"error_description"
|
|
1258
|
+
];
|
|
1259
|
+
var UPSTREAM_ERROR_MAX_LEN = 200;
|
|
1260
|
+
var clampUpstreamMessage = (value) => {
|
|
1261
|
+
const trimmed = value.trim();
|
|
1262
|
+
if (!trimmed) return "";
|
|
1263
|
+
return trimmed.length > UPSTREAM_ERROR_MAX_LEN ? `${trimmed.slice(0, UPSTREAM_ERROR_MAX_LEN)}\u2026` : trimmed;
|
|
1264
|
+
};
|
|
1265
|
+
var extractUpstreamErrorMessage = (body) => {
|
|
1266
|
+
const parsed = tryParseJson(body);
|
|
1267
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1268
|
+
const clipped = clampUpstreamMessage(body);
|
|
1269
|
+
return clipped || void 0;
|
|
1270
|
+
}
|
|
1271
|
+
const record = parsed;
|
|
1272
|
+
for (const field of UPSTREAM_ERROR_FIELDS) {
|
|
1273
|
+
const value = record[field];
|
|
1274
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1275
|
+
return clampUpstreamMessage(value);
|
|
1276
|
+
}
|
|
1277
|
+
if (value && typeof value === "object") {
|
|
1278
|
+
const nested = value.message;
|
|
1279
|
+
if (typeof nested === "string" && nested.length > 0) {
|
|
1280
|
+
return clampUpstreamMessage(nested);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return void 0;
|
|
1285
|
+
};
|
|
1286
|
+
var resolveRequestBody = (rawData, readStdin2) => {
|
|
1252
1287
|
const fromFile = (spec) => {
|
|
1253
1288
|
if (spec === "@-") return readFileSync3(0);
|
|
1254
1289
|
return readFileSync3(resolvePath(spec.slice(1)));
|
|
1255
1290
|
};
|
|
1256
|
-
if (
|
|
1291
|
+
if (readStdin2 && rawData !== void 0) {
|
|
1257
1292
|
throw new Error(
|
|
1258
1293
|
"Conflicting body sources: use either --data-stdin or -d, not both."
|
|
1259
1294
|
);
|
|
1260
1295
|
}
|
|
1261
1296
|
let body;
|
|
1262
|
-
|
|
1297
|
+
let cap;
|
|
1298
|
+
if (readStdin2) {
|
|
1263
1299
|
body = readFileSync3(0);
|
|
1300
|
+
cap = MAX_FILE_REQUEST_BODY_BYTES;
|
|
1264
1301
|
} else if (rawData?.startsWith("@")) {
|
|
1265
1302
|
body = fromFile(rawData);
|
|
1303
|
+
cap = MAX_FILE_REQUEST_BODY_BYTES;
|
|
1266
1304
|
} else {
|
|
1267
1305
|
body = rawData;
|
|
1306
|
+
cap = MAX_INLINE_REQUEST_BODY_BYTES;
|
|
1268
1307
|
}
|
|
1269
1308
|
if (body !== void 0) {
|
|
1270
1309
|
const bytes = Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body, "utf8");
|
|
1271
|
-
if (bytes >
|
|
1310
|
+
if (bytes > cap) {
|
|
1272
1311
|
throw new Error(
|
|
1273
|
-
`Request body is ${bytes} bytes \u2014 exceeds the ${
|
|
1312
|
+
`Request body is ${bytes} bytes \u2014 exceeds the ${cap} byte local cap. This is a CLI-side guard, not a protocol limit. Compress the payload or contact us if you need it raised.`
|
|
1274
1313
|
);
|
|
1275
1314
|
}
|
|
1276
1315
|
}
|
|
@@ -1381,14 +1420,16 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1381
1420
|
capName = cap.name;
|
|
1382
1421
|
}
|
|
1383
1422
|
const searchResult = await apiService.search({ query: capName });
|
|
1384
|
-
const
|
|
1423
|
+
const matchedEntry = searchResult.capabilities.find(
|
|
1385
1424
|
(c) => c.slug === options.capability || c.id === options.capability
|
|
1386
1425
|
);
|
|
1426
|
+
const slugFoundInResults = Boolean(matchedEntry);
|
|
1387
1427
|
stateService.saveLastSearch({
|
|
1388
1428
|
searchId: searchResult.searchId,
|
|
1389
1429
|
capabilities: searchResult.capabilities.map((c) => ({
|
|
1390
1430
|
position: c.position,
|
|
1391
1431
|
id: c.id,
|
|
1432
|
+
slug: c.slug,
|
|
1392
1433
|
url: c.url,
|
|
1393
1434
|
urlTemplate: c.urlTemplate ?? null,
|
|
1394
1435
|
displayCostAmount: c.cost.amount
|
|
@@ -1406,7 +1447,13 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1406
1447
|
slugFoundInResults
|
|
1407
1448
|
});
|
|
1408
1449
|
analyticsService.capture("capability_viewed", {
|
|
1450
|
+
// Existing field preserved as the raw --capability
|
|
1451
|
+
// input for back-compat.
|
|
1409
1452
|
capabilityId: options.capability,
|
|
1453
|
+
// New canonical identifier fields hydrated from the
|
|
1454
|
+
// resolved search result (when the slug was found).
|
|
1455
|
+
capabilityUid: matchedEntry?.id,
|
|
1456
|
+
capabilitySlug: matchedEntry?.slug,
|
|
1410
1457
|
fromLastSearch: false,
|
|
1411
1458
|
searchId: searchResult.searchId,
|
|
1412
1459
|
triggeredBy: "slug_handoff"
|
|
@@ -1423,7 +1470,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1423
1470
|
} catch (err) {
|
|
1424
1471
|
const message = err instanceof Error ? err.message : "Failed to read request body";
|
|
1425
1472
|
analyticsService.capture("fetch_error", {
|
|
1426
|
-
cliErrorClass: /exceeds the .* byte
|
|
1473
|
+
cliErrorClass: /exceeds the .* byte local cap/.test(message) ? "payload_too_large" : "schema_validation_failed",
|
|
1427
1474
|
url: redactUrl(resolvedUrl),
|
|
1428
1475
|
error: truncateError(message)
|
|
1429
1476
|
});
|
|
@@ -1465,6 +1512,8 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1465
1512
|
});
|
|
1466
1513
|
const matchCtx = explicitCapabilityCtx ?? urlCtx;
|
|
1467
1514
|
const capabilityId = options.capability ?? matchCtx?.capabilityId ?? null;
|
|
1515
|
+
const capabilityUid = matchCtx?.capabilityUid ?? null;
|
|
1516
|
+
const capabilitySlug = matchCtx?.capabilitySlug ?? null;
|
|
1468
1517
|
const searchId = matchCtx?.searchId;
|
|
1469
1518
|
const resultRank = matchCtx?.resultRank;
|
|
1470
1519
|
const matchedDisplayCostAmount = matchCtx?.displayCostAmount;
|
|
@@ -1614,10 +1663,12 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1614
1663
|
const isFailure = !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
|
|
1615
1664
|
let errorSnippetHash;
|
|
1616
1665
|
let errorSnippetLength;
|
|
1666
|
+
let upstreamErrorMessage;
|
|
1617
1667
|
if (isFailure && body && !bodyIsBinary) {
|
|
1618
1668
|
const snippet = body.slice(0, 500);
|
|
1619
1669
|
errorSnippetHash = createHash3("sha256").update(snippet).digest("hex");
|
|
1620
1670
|
errorSnippetLength = snippet.length;
|
|
1671
|
+
upstreamErrorMessage = extractUpstreamErrorMessage(body);
|
|
1621
1672
|
}
|
|
1622
1673
|
let runId = null;
|
|
1623
1674
|
if (capabilityId && apiService.walletAddress) {
|
|
@@ -1661,7 +1712,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1661
1712
|
);
|
|
1662
1713
|
}
|
|
1663
1714
|
}
|
|
1664
|
-
const outcome = !finalResponse ? "network_error" : status === 402 && !paymentMeta ? "payment_failed" : status !== void 0 && status >= 400 && status !== 402 ? "server_error" : "success";
|
|
1715
|
+
const outcome = !finalResponse ? "network_error" : status === 402 && !paymentMeta ? "payment_failed" : status === 402 && paymentMeta ? "payment_rejected" : status !== void 0 && status >= 400 && status !== 402 ? "server_error" : "success";
|
|
1665
1716
|
analyticsService.capture("fetch_executed", {
|
|
1666
1717
|
url: redactUrl(resolvedUrl),
|
|
1667
1718
|
status,
|
|
@@ -1670,6 +1721,13 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1670
1721
|
hasPayment: !!paymentMeta,
|
|
1671
1722
|
paymentProtocol: paymentMeta?.protocol,
|
|
1672
1723
|
paymentAmount: paymentMeta?.amount,
|
|
1724
|
+
// Canonical join keys — capabilitySlug is the dashboard
|
|
1725
|
+
// breakdown dimension; capabilityUid is the stable join
|
|
1726
|
+
// key with the capabilities table. capabilityId is the
|
|
1727
|
+
// back-compat alias (same value as capabilityUid when
|
|
1728
|
+
// resolved, else the raw --capability input).
|
|
1729
|
+
capabilityUid: capabilityUid ?? void 0,
|
|
1730
|
+
capabilitySlug: capabilitySlug ?? void 0,
|
|
1673
1731
|
capabilityId: capabilityId ?? void 0,
|
|
1674
1732
|
searchId: searchId ?? void 0,
|
|
1675
1733
|
resultRank: resultRank ?? void 0,
|
|
@@ -1679,15 +1737,21 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1679
1737
|
});
|
|
1680
1738
|
const isFetchFailure = Boolean(fetchError) || !finalResponse || typeof status === "number" && (status < 200 || status >= 300);
|
|
1681
1739
|
if (isFetchFailure) {
|
|
1682
|
-
const cliErrorClass = fetchError || !finalResponse ? "network" : !apiService.walletAddress ? "auth_missing" : "unknown";
|
|
1740
|
+
const cliErrorClass = fetchError || !finalResponse ? "network" : !apiService.walletAddress ? "auth_missing" : status === 402 && paymentMeta ? "payment_rejected" : typeof status === "number" && status >= 500 ? "upstream_5xx" : typeof status === "number" && status >= 400 ? "upstream_4xx" : "unknown";
|
|
1683
1741
|
analyticsService.capture("fetch_error", {
|
|
1684
1742
|
cliErrorClass,
|
|
1743
|
+
capabilityUid: capabilityUid ?? void 0,
|
|
1744
|
+
capabilitySlug: capabilitySlug ?? void 0,
|
|
1685
1745
|
capabilityId: capabilityId ?? void 0,
|
|
1686
1746
|
searchId: searchId ?? void 0,
|
|
1687
1747
|
resultRank: resultRank ?? void 0,
|
|
1688
1748
|
url: redactUrl(resolvedUrl),
|
|
1749
|
+
status: typeof status === "number" ? status : void 0,
|
|
1750
|
+
upstreamErrorMessage,
|
|
1751
|
+
errorSnippetHash,
|
|
1752
|
+
errorSnippetLength,
|
|
1689
1753
|
error: truncateError(
|
|
1690
|
-
fetchError?.message ?? skipReasons.join("; ")
|
|
1754
|
+
fetchError?.message ?? upstreamErrorMessage ?? skipReasons.join("; ")
|
|
1691
1755
|
),
|
|
1692
1756
|
skippedRun: !runId && skipReasons.length > 0
|
|
1693
1757
|
});
|
|
@@ -1695,6 +1759,16 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1695
1759
|
if (fetchError && !options.json) {
|
|
1696
1760
|
console.error(` Fetch failed: ${fetchError.message}`);
|
|
1697
1761
|
}
|
|
1762
|
+
if (finalResponse && !fetchError && !options.json && typeof status === "number" && (status < 200 || status >= 300)) {
|
|
1763
|
+
const messageSuffix = upstreamErrorMessage ? ` \u2014 ${upstreamErrorMessage}` : "";
|
|
1764
|
+
if (status === 402 && paymentMeta) {
|
|
1765
|
+
console.error(
|
|
1766
|
+
` Upstream returned 402 after payment was sent \u2014 the seller's facilitator rejected the credential${messageSuffix}`
|
|
1767
|
+
);
|
|
1768
|
+
} else {
|
|
1769
|
+
console.error(` Upstream returned ${status}${messageSuffix}`);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1698
1772
|
if (options.json) {
|
|
1699
1773
|
const responseStatus = finalResponse?.status ?? null;
|
|
1700
1774
|
const ok = responseStatus !== null && responseStatus >= 200 && responseStatus < 300;
|
|
@@ -1775,10 +1849,19 @@ var fetchCommand = (appContext) => new Command3("fetch").description(
|
|
|
1775
1849
|
|
|
1776
1850
|
// src/commands/get-command.ts
|
|
1777
1851
|
import { Command as Command4 } from "commander";
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1852
|
+
|
|
1853
|
+
// src/util/format-price.ts
|
|
1854
|
+
var centsToDollars = (cents) => {
|
|
1855
|
+
const value = Number.parseFloat(cents) / 100;
|
|
1856
|
+
if (!Number.isFinite(value) || value === 0) return "0.00";
|
|
1857
|
+
if (value < 1e-4) return value.toFixed(6);
|
|
1858
|
+
if (value < 1e-3) return value.toFixed(5);
|
|
1859
|
+
if (value < 0.01) return value.toFixed(4);
|
|
1860
|
+
if (value < 1) return value.toFixed(3);
|
|
1861
|
+
return value.toFixed(2);
|
|
1781
1862
|
};
|
|
1863
|
+
|
|
1864
|
+
// src/commands/get-command.ts
|
|
1782
1865
|
var formatRelativeTimestamp = (iso) => {
|
|
1783
1866
|
if (!iso) return "never";
|
|
1784
1867
|
const then = new Date(iso).getTime();
|
|
@@ -1795,22 +1878,6 @@ var formatRelativeTimestamp = (iso) => {
|
|
|
1795
1878
|
if (diffMo < 12) return `${diffMo}mo ago`;
|
|
1796
1879
|
return `${Math.round(diffMo / 12)}y ago`;
|
|
1797
1880
|
};
|
|
1798
|
-
var formatTrustScore = (capability) => {
|
|
1799
|
-
if (capability.trustScore != null) {
|
|
1800
|
-
return `Trust Score: ${capability.trustScore}/100`;
|
|
1801
|
-
}
|
|
1802
|
-
return "Trust Score: --";
|
|
1803
|
-
};
|
|
1804
|
-
var formatTrustComponent = (label, value) => {
|
|
1805
|
-
const display = value != null ? `${value}/100` : "--";
|
|
1806
|
-
return ` ${label.padEnd(22)}${display}`;
|
|
1807
|
-
};
|
|
1808
|
-
var centsToDollars = (cents) => {
|
|
1809
|
-
const value = Number.parseFloat(cents) / 100;
|
|
1810
|
-
if (value < 0.01) return value.toFixed(4);
|
|
1811
|
-
if (value < 1) return value.toFixed(3);
|
|
1812
|
-
return value.toFixed(2);
|
|
1813
|
-
};
|
|
1814
1881
|
var formatCost = (capability) => {
|
|
1815
1882
|
const lines = [];
|
|
1816
1883
|
const observed = capability.priceObserved;
|
|
@@ -1820,6 +1887,10 @@ var formatCost = (capability) => {
|
|
|
1820
1887
|
const median = observed.medianCents ? centsToDollars(observed.medianCents) : null;
|
|
1821
1888
|
const detail = median ? `median $${median}, n=${observed.sampleCount}` : `n=${observed.sampleCount}`;
|
|
1822
1889
|
lines.push(` Cost: $${min}\u2013$${max}/call (${detail})`);
|
|
1890
|
+
} else if (capability.displayCostAmount === "0") {
|
|
1891
|
+
lines.push(` Cost: Free`);
|
|
1892
|
+
} else if (capability.displayCostAmount === "unknown") {
|
|
1893
|
+
lines.push(` Cost: variable pricing`);
|
|
1823
1894
|
} else {
|
|
1824
1895
|
lines.push(` Cost: $${capability.displayCostAmount}/call`);
|
|
1825
1896
|
}
|
|
@@ -1829,6 +1900,10 @@ var formatCost = (capability) => {
|
|
|
1829
1900
|
}
|
|
1830
1901
|
return lines;
|
|
1831
1902
|
};
|
|
1903
|
+
var formatReviewCount = (count) => {
|
|
1904
|
+
if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
|
|
1905
|
+
return count.toString();
|
|
1906
|
+
};
|
|
1832
1907
|
var formatRating = (rating) => {
|
|
1833
1908
|
if (rating.state === "unrated") return "unrated";
|
|
1834
1909
|
const successPct = `${Math.round(Number.parseFloat(rating.successRate) * 100)}%`;
|
|
@@ -1915,35 +1990,20 @@ var buildTryItExample = (capability) => {
|
|
|
1915
1990
|
var formatCapability = (capability) => {
|
|
1916
1991
|
const lines = [];
|
|
1917
1992
|
lines.push(capability.name);
|
|
1918
|
-
lines.push(` ${formatTrustScore(capability)}`);
|
|
1919
|
-
if (capability.trustComponents) {
|
|
1920
|
-
lines.push(
|
|
1921
|
-
formatTrustComponent(
|
|
1922
|
-
"API Quality:",
|
|
1923
|
-
capability.trustComponents.apiQuality
|
|
1924
|
-
)
|
|
1925
|
-
);
|
|
1926
|
-
lines.push(
|
|
1927
|
-
formatTrustComponent(
|
|
1928
|
-
"Blockchain Activity:",
|
|
1929
|
-
capability.trustComponents.blockchainActivity
|
|
1930
|
-
)
|
|
1931
|
-
);
|
|
1932
|
-
lines.push(
|
|
1933
|
-
formatTrustComponent(
|
|
1934
|
-
"Performance:",
|
|
1935
|
-
capability.trustComponents.performance
|
|
1936
|
-
)
|
|
1937
|
-
);
|
|
1938
|
-
}
|
|
1939
1993
|
lines.push(` Rating: ${formatRating(capability.rating)}`);
|
|
1940
|
-
lines.push(` Status: ${capability.
|
|
1994
|
+
lines.push(` Status: ${capability.displayStatus ?? "unknown"}`);
|
|
1941
1995
|
lines.push(...formatCost(capability));
|
|
1942
1996
|
lines.push(` URL: ${capability.url}`);
|
|
1943
1997
|
lines.push(` Method: ${capability.method}`);
|
|
1944
1998
|
lines.push(
|
|
1945
1999
|
` Last successful run: ${formatRelativeTimestamp(capability.lastSuccessfullyRanAt)}`
|
|
1946
2000
|
);
|
|
2001
|
+
if (capability.exampleAgentPrompt?.trim()) {
|
|
2002
|
+
lines.push(" Example prompt:");
|
|
2003
|
+
for (const promptLine of capability.exampleAgentPrompt.split("\n")) {
|
|
2004
|
+
lines.push(` ${promptLine}`);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
1947
2007
|
lines.push(...buildTryItExample(capability));
|
|
1948
2008
|
return lines.join("\n");
|
|
1949
2009
|
};
|
|
@@ -1952,7 +2012,7 @@ var getCommand = (appContext) => new Command4("get").description(
|
|
|
1952
2012
|
).argument(
|
|
1953
2013
|
"<identifier>",
|
|
1954
2014
|
"Position number from search results, or a capability slug"
|
|
1955
|
-
).option("--formatted", "Output formatted
|
|
2015
|
+
).option("--formatted", "Output formatted capability details").option(
|
|
1956
2016
|
"--agent <name>",
|
|
1957
2017
|
"Identify your agent host for this invocation. Overrides auto-detect for this call only."
|
|
1958
2018
|
).action(async (identifier, options) => {
|
|
@@ -1996,7 +2056,15 @@ var getCommand = (appContext) => new Command4("get").description(
|
|
|
1996
2056
|
console.log(JSON.stringify(capability, null, 2));
|
|
1997
2057
|
}
|
|
1998
2058
|
analyticsService.capture("capability_viewed", {
|
|
2059
|
+
// Existing field — raw user input (uid, slug, or position
|
|
2060
|
+
// resolved to uid via the last-search cache). Preserved
|
|
2061
|
+
// for back-compat with existing dashboards.
|
|
1999
2062
|
capabilityId,
|
|
2063
|
+
// New canonical identifier fields read off the API response,
|
|
2064
|
+
// so they are always populated regardless of how the user
|
|
2065
|
+
// referenced the capability.
|
|
2066
|
+
capabilityUid: capability.uid,
|
|
2067
|
+
capabilitySlug: capability.slug,
|
|
2000
2068
|
fromLastSearch: isPosition,
|
|
2001
2069
|
...isPosition ? { position } : {},
|
|
2002
2070
|
...searchId ? { searchId } : {}
|
|
@@ -2130,7 +2198,12 @@ var printReadyFooter = () => {
|
|
|
2130
2198
|
const lines = [
|
|
2131
2199
|
"",
|
|
2132
2200
|
` ${color.boldGreen("Zero is ready!")} Zero works best with an AI agent.`,
|
|
2133
|
-
|
|
2201
|
+
"",
|
|
2202
|
+
" First, claim your $5 welcome bonus \u2014 run this now, before opening your agent:",
|
|
2203
|
+
"",
|
|
2204
|
+
` ${color.cyan("zero welcome")}`,
|
|
2205
|
+
"",
|
|
2206
|
+
` Then open ${color.boldRed("Claude Code")}, Codex, Cursor, Blackbox, or your agent of choice`,
|
|
2134
2207
|
" and try this prompt to get started:",
|
|
2135
2208
|
"",
|
|
2136
2209
|
` ${color.cyan("What is zero and how do I use it?")}`,
|
|
@@ -2593,12 +2666,13 @@ To remove them, run: ${color.cyan("zero init cleanup")}`
|
|
|
2593
2666
|
sectionDivider();
|
|
2594
2667
|
console.error(printReadyFooter());
|
|
2595
2668
|
currentStep = "complete";
|
|
2669
|
+
if (walletAddress) {
|
|
2670
|
+
appContext.services.analyticsService.setWalletAddress(walletAddress);
|
|
2671
|
+
}
|
|
2596
2672
|
appContext.services.analyticsService.capture("wallet_initialized", {
|
|
2597
2673
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
2598
2674
|
wallet_created: walletCreated,
|
|
2599
2675
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
2600
|
-
wallet_address: walletAddress,
|
|
2601
|
-
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
2602
2676
|
agents_detected: agentsDetected,
|
|
2603
2677
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
2604
2678
|
agents_detected_count: agentsDetected.length,
|
|
@@ -2922,7 +2996,7 @@ var formatReviewCount2 = (count) => {
|
|
|
2922
2996
|
return count.toString();
|
|
2923
2997
|
};
|
|
2924
2998
|
var formatRatingBadge = (rating) => {
|
|
2925
|
-
if (rating.state === "unrated") return "
|
|
2999
|
+
if (rating.state === "unrated") return "unrated";
|
|
2926
3000
|
const successPct = `${Math.round(Number.parseFloat(rating.successRate) * 100)}%`;
|
|
2927
3001
|
const reviews = formatReviewCount2(rating.reviews);
|
|
2928
3002
|
if (rating.stars) {
|
|
@@ -2930,13 +3004,7 @@ var formatRatingBadge = (rating) => {
|
|
|
2930
3004
|
}
|
|
2931
3005
|
return `${successPct} success \xB7 ${reviews} reviews`;
|
|
2932
3006
|
};
|
|
2933
|
-
var
|
|
2934
|
-
if (item.trustScore != null) {
|
|
2935
|
-
return `Trust: ${item.trustScore}`;
|
|
2936
|
-
}
|
|
2937
|
-
return "Trust: --";
|
|
2938
|
-
};
|
|
2939
|
-
var formatHealthBadge = (status) => {
|
|
3007
|
+
var formatStatusBadge = (status) => {
|
|
2940
3008
|
if (!status || status === "unknown") return "";
|
|
2941
3009
|
return ` \u2014 ${status}`;
|
|
2942
3010
|
};
|
|
@@ -2946,21 +3014,20 @@ var formatSearchResults = (results) => {
|
|
|
2946
3014
|
const baseName = r.canonicalName ?? r.name;
|
|
2947
3015
|
const displayName = r.brandName ? `${r.brandName} ${baseName}` : baseName;
|
|
2948
3016
|
const displayDescription = r.whatItDoes ?? r.description;
|
|
2949
|
-
const trustBadge = formatTrustBadge(r);
|
|
2950
3017
|
const ratingBadge = formatRatingBadge(r.rating);
|
|
2951
|
-
const
|
|
2952
|
-
const
|
|
2953
|
-
return ` ${r.position}. ${displayName} \u2014
|
|
3018
|
+
const statusBadge = formatStatusBadge(r.displayStatus);
|
|
3019
|
+
const costLabel = r.cost.amount === "0" ? "Free" : r.cost.amount === "unknown" ? "variable pricing" : `$${r.cost.amount}/call`;
|
|
3020
|
+
return ` ${r.position}. ${displayName} \u2014 ${costLabel} \u2014 ${ratingBadge}${statusBadge}
|
|
2954
3021
|
"${displayDescription}"`;
|
|
2955
3022
|
}).join("\n");
|
|
2956
3023
|
};
|
|
2957
3024
|
var searchCommand = (appContext) => new Command8("search").description("Search for capabilities").argument("<query>", "Search query").option("--json", "Output raw JSON to stdout").option("--offset <n>", "Pagination offset", Number).option("--limit <n>", "Results per page", Number).option("--free", "Only show free capabilities").option(
|
|
2958
3025
|
"--max-cost <amount>",
|
|
2959
3026
|
`Maximum cost per call in USD (default: ${DEFAULT_MAX_COST_USD})`
|
|
2960
|
-
).option("--
|
|
3027
|
+
).option("--protocol <protocol>", "Payment protocol (x402 or mpp)").option(
|
|
2961
3028
|
"--status <status>",
|
|
2962
3029
|
"Filter by availability (healthy, degraded, down)"
|
|
2963
|
-
).option("--all", "
|
|
3030
|
+
).option("--all", "Disable default quality filtering").option(
|
|
2964
3031
|
"--source <source>",
|
|
2965
3032
|
"Only show results from this crawl source (e.g. mpp, bazaar)"
|
|
2966
3033
|
).option(
|
|
@@ -3000,9 +3067,7 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
|
|
|
3000
3067
|
limit: options.limit,
|
|
3001
3068
|
freeOnly: options.free,
|
|
3002
3069
|
maxCost: effectiveMaxCost,
|
|
3003
|
-
minRating: options.minRating,
|
|
3004
3070
|
protocol: options.protocol,
|
|
3005
|
-
minTrust: options.minTrust,
|
|
3006
3071
|
availabilityStatus: options.status,
|
|
3007
3072
|
includeAll: options.all,
|
|
3008
3073
|
source: options.source,
|
|
@@ -3020,13 +3085,11 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
|
|
|
3020
3085
|
freeOnly: options.free ?? false,
|
|
3021
3086
|
maxCost: effectiveMaxCost,
|
|
3022
3087
|
maxCostDefaulted: appliedDefaultMaxCost,
|
|
3023
|
-
minRating: options.minRating,
|
|
3024
3088
|
protocol: options.protocol,
|
|
3025
|
-
minTrust: options.minTrust,
|
|
3026
3089
|
availabilityStatus: options.status,
|
|
3027
3090
|
includeAll: options.all ?? false,
|
|
3028
|
-
|
|
3029
|
-
|
|
3091
|
+
listingSource: options.source,
|
|
3092
|
+
excludeListingSource: options.excludeSource,
|
|
3030
3093
|
json: options.json ?? false
|
|
3031
3094
|
});
|
|
3032
3095
|
if (options.json) {
|
|
@@ -3048,6 +3111,7 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
|
|
|
3048
3111
|
capabilities: result.capabilities.map((c) => ({
|
|
3049
3112
|
position: c.position,
|
|
3050
3113
|
id: c.id,
|
|
3114
|
+
slug: c.slug,
|
|
3051
3115
|
url: c.url,
|
|
3052
3116
|
urlTemplate: c.urlTemplate ?? null,
|
|
3053
3117
|
displayCostAmount: c.cost.amount
|
|
@@ -3098,9 +3162,43 @@ import { homedir as homedir3 } from "os";
|
|
|
3098
3162
|
import { join as join3 } from "path";
|
|
3099
3163
|
import { Command as Command10 } from "commander";
|
|
3100
3164
|
import open from "open";
|
|
3101
|
-
import {
|
|
3102
|
-
|
|
3103
|
-
|
|
3165
|
+
import { isAddress } from "viem";
|
|
3166
|
+
import { generatePrivateKey as generatePrivateKey2, privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
3167
|
+
|
|
3168
|
+
// src/util/stdin.ts
|
|
3169
|
+
var readStdin = async () => {
|
|
3170
|
+
const chunks = [];
|
|
3171
|
+
for await (const chunk of process.stdin) {
|
|
3172
|
+
chunks.push(chunk);
|
|
3173
|
+
}
|
|
3174
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
3175
|
+
};
|
|
3176
|
+
|
|
3177
|
+
// src/commands/wallet-command.ts
|
|
3178
|
+
var PRIVATE_KEY_PATTERN = /^0x[0-9a-fA-F]{64}$/;
|
|
3179
|
+
var parseProvider = (raw) => raw === "stripe" ? "stripe" : "coinbase";
|
|
3180
|
+
var walletBalanceCommand = (appContext) => new Command10("balance").description("Show wallet balance").option(
|
|
3181
|
+
"--address <address>",
|
|
3182
|
+
"Check balance for an arbitrary address (no key needed). Useful with `zero wallet generate` \u2014 verify funds arrived without setting the wallet as your default."
|
|
3183
|
+
).action(async (options) => {
|
|
3184
|
+
const { walletService, paymentService } = appContext.services;
|
|
3185
|
+
if (options.address) {
|
|
3186
|
+
if (!isAddress(options.address)) {
|
|
3187
|
+
console.error("Invalid address (expected 0x + 40 hex chars).");
|
|
3188
|
+
process.exitCode = 1;
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
try {
|
|
3192
|
+
const balance2 = await paymentService.getTotalBalanceForAddress(
|
|
3193
|
+
options.address
|
|
3194
|
+
);
|
|
3195
|
+
console.log(`${balance2.amount} ${balance2.asset}`);
|
|
3196
|
+
} catch {
|
|
3197
|
+
console.error("Failed to fetch balance");
|
|
3198
|
+
process.exitCode = 1;
|
|
3199
|
+
}
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3104
3202
|
const balance = await walletService.getBalance();
|
|
3105
3203
|
if (balance === null) {
|
|
3106
3204
|
console.error("No wallet configured. Run `zero init` first.");
|
|
@@ -3114,6 +3212,20 @@ var walletBalanceCommand = (appContext) => new Command10("balance").description(
|
|
|
3114
3212
|
}
|
|
3115
3213
|
console.log(`${balance.amount} ${balance.asset}`);
|
|
3116
3214
|
});
|
|
3215
|
+
var readPrivateKeyFromStdin = async () => {
|
|
3216
|
+
if (process.stdin.isTTY) {
|
|
3217
|
+
console.error(
|
|
3218
|
+
"Expected a private key on stdin. Example:\n zero wallet generate --json | jq -r .privateKey | zero wallet fund --key-stdin"
|
|
3219
|
+
);
|
|
3220
|
+
return null;
|
|
3221
|
+
}
|
|
3222
|
+
const raw = (await readStdin()).trim();
|
|
3223
|
+
if (!PRIVATE_KEY_PATTERN.test(raw)) {
|
|
3224
|
+
console.error("Invalid private key on stdin (expected 0x + 64 hex chars).");
|
|
3225
|
+
return null;
|
|
3226
|
+
}
|
|
3227
|
+
return raw;
|
|
3228
|
+
};
|
|
3117
3229
|
var walletFundCommand = (appContext) => new Command10("fund").description("Fund your wallet").argument("[amount]", "Amount to fund in USDC").option("--manual", "Show wallet address for manual transfer").option(
|
|
3118
3230
|
"--no-open",
|
|
3119
3231
|
"Print the funding URL instead of opening a browser (for agents \u2014 funding links are one-time use, hand the URL to the user)"
|
|
@@ -3121,9 +3233,55 @@ var walletFundCommand = (appContext) => new Command10("fund").description("Fund
|
|
|
3121
3233
|
"--use <provider>",
|
|
3122
3234
|
"Onramp provider: coinbase or stripe",
|
|
3123
3235
|
"coinbase"
|
|
3236
|
+
).option(
|
|
3237
|
+
"--key-stdin",
|
|
3238
|
+
"Read a private key from stdin and mint a funding URL for that wallet instead of your configured one. Useful with `zero wallet generate`. Does not modify your config."
|
|
3124
3239
|
).action(
|
|
3125
3240
|
async (amount, options) => {
|
|
3126
|
-
const { analyticsService, walletService } = appContext.services;
|
|
3241
|
+
const { analyticsService, apiService, walletService } = appContext.services;
|
|
3242
|
+
const provider = parseProvider(options.use);
|
|
3243
|
+
if (options.keyStdin) {
|
|
3244
|
+
const key = await readPrivateKeyFromStdin();
|
|
3245
|
+
if (!key) {
|
|
3246
|
+
process.exitCode = 1;
|
|
3247
|
+
return;
|
|
3248
|
+
}
|
|
3249
|
+
const altAccount = privateKeyToAccount2(key);
|
|
3250
|
+
const altApi = apiService.withAccount(altAccount);
|
|
3251
|
+
if (options.manual) {
|
|
3252
|
+
console.log(`Send USDC (Base) to:
|
|
3253
|
+
${altAccount.address}`);
|
|
3254
|
+
analyticsService.capture("wallet_funded", {
|
|
3255
|
+
method: "manual",
|
|
3256
|
+
amount,
|
|
3257
|
+
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3258
|
+
alt_wallet: true
|
|
3259
|
+
});
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3262
|
+
const url2 = await altApi.getFundingUrl(amount, provider);
|
|
3263
|
+
if (!url2) {
|
|
3264
|
+
console.log(`Send USDC (Base) to:
|
|
3265
|
+
${altAccount.address}`);
|
|
3266
|
+
console.error(
|
|
3267
|
+
"Could not get funding URL. Send USDC (Base) manually."
|
|
3268
|
+
);
|
|
3269
|
+
process.exitCode = 1;
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
console.log(
|
|
3273
|
+
"Funding URL (one-time use \u2014 open it in a browser to fund):"
|
|
3274
|
+
);
|
|
3275
|
+
console.log(url2);
|
|
3276
|
+
console.log(`Wallet address: ${altAccount.address}`);
|
|
3277
|
+
analyticsService.capture("wallet_funded", {
|
|
3278
|
+
method: "url",
|
|
3279
|
+
amount,
|
|
3280
|
+
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3281
|
+
alt_wallet: true
|
|
3282
|
+
});
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3127
3285
|
const address = walletService.getAddress();
|
|
3128
3286
|
if (!address) {
|
|
3129
3287
|
console.error("No wallet configured. Run `zero init` first.");
|
|
@@ -3139,11 +3297,7 @@ ${address}`);
|
|
|
3139
3297
|
});
|
|
3140
3298
|
return;
|
|
3141
3299
|
}
|
|
3142
|
-
const
|
|
3143
|
-
const url = await appContext.services.apiService.getFundingUrl(
|
|
3144
|
-
amount,
|
|
3145
|
-
provider
|
|
3146
|
-
);
|
|
3300
|
+
const url = await apiService.getFundingUrl(amount, provider);
|
|
3147
3301
|
if (url) {
|
|
3148
3302
|
if (options.open) {
|
|
3149
3303
|
await open(url);
|
|
@@ -3224,18 +3378,95 @@ var walletSetCommand = (appContext) => new Command10("set").description("Set wal
|
|
|
3224
3378
|
)
|
|
3225
3379
|
);
|
|
3226
3380
|
console.log(`Wallet set: ${account.address}`);
|
|
3381
|
+
analyticsService.setWalletAddress(account.address);
|
|
3227
3382
|
analyticsService.capture("wallet_set", {
|
|
3228
|
-
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3229
|
-
wallet_address: account.address,
|
|
3230
3383
|
force: options.force ?? false
|
|
3231
3384
|
});
|
|
3232
3385
|
});
|
|
3386
|
+
var walletGenerateCommand = (appContext) => new Command10("generate").description(
|
|
3387
|
+
"Generate a fresh wallet (address + private key) without touching your configured wallet"
|
|
3388
|
+
).option("--json", "Emit { address, privateKey } as JSON").option(
|
|
3389
|
+
"--fund",
|
|
3390
|
+
"Also mint a one-time onramp URL for the new wallet (does not auto-open)"
|
|
3391
|
+
).option("--amount <amount>", "With --fund: USDC amount to pre-fill").option(
|
|
3392
|
+
"--use <provider>",
|
|
3393
|
+
"With --fund: onramp provider (coinbase | stripe)",
|
|
3394
|
+
"coinbase"
|
|
3395
|
+
).action(
|
|
3396
|
+
async (options) => {
|
|
3397
|
+
const { analyticsService, apiService } = appContext.services;
|
|
3398
|
+
const privateKey = generatePrivateKey2();
|
|
3399
|
+
const account = privateKeyToAccount2(privateKey);
|
|
3400
|
+
let fundingUrl = null;
|
|
3401
|
+
if (options.fund) {
|
|
3402
|
+
const provider = parseProvider(options.use);
|
|
3403
|
+
fundingUrl = await apiService.withAccount(account).getFundingUrl(options.amount, provider);
|
|
3404
|
+
}
|
|
3405
|
+
if (options.json) {
|
|
3406
|
+
const payload = {
|
|
3407
|
+
address: account.address,
|
|
3408
|
+
privateKey
|
|
3409
|
+
};
|
|
3410
|
+
if (options.fund) {
|
|
3411
|
+
payload.fundingUrl = fundingUrl;
|
|
3412
|
+
}
|
|
3413
|
+
console.log(JSON.stringify(payload));
|
|
3414
|
+
} else {
|
|
3415
|
+
console.log("");
|
|
3416
|
+
console.log(
|
|
3417
|
+
"New wallet generated. Save these \u2014 they are NOT stored anywhere."
|
|
3418
|
+
);
|
|
3419
|
+
console.log("");
|
|
3420
|
+
console.log(
|
|
3421
|
+
" WARNING: Anyone with this private key can spend any funds sent"
|
|
3422
|
+
);
|
|
3423
|
+
console.log(
|
|
3424
|
+
" to the address. Don't paste it into chat, commit it to git, or"
|
|
3425
|
+
);
|
|
3426
|
+
console.log(" share your screen with it visible.");
|
|
3427
|
+
console.log("");
|
|
3428
|
+
console.log(` Address: ${account.address}`);
|
|
3429
|
+
console.log(` Private key: ${privateKey}`);
|
|
3430
|
+
console.log("");
|
|
3431
|
+
if (options.fund) {
|
|
3432
|
+
if (fundingUrl) {
|
|
3433
|
+
console.log("Funding URL (one-time use):");
|
|
3434
|
+
console.log(` ${fundingUrl}`);
|
|
3435
|
+
console.log("");
|
|
3436
|
+
} else {
|
|
3437
|
+
console.log(
|
|
3438
|
+
"Could not mint a funding URL \u2014 fund the address manually with USDC (Base)."
|
|
3439
|
+
);
|
|
3440
|
+
console.log("");
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
console.log("Next steps:");
|
|
3444
|
+
if (!options.fund) {
|
|
3445
|
+
console.log(" - Fund the address with USDC (Base) before use.");
|
|
3446
|
+
}
|
|
3447
|
+
console.log(
|
|
3448
|
+
` - Use it for a single command: ZERO_PRIVATE_KEY=${privateKey} zero <command>`
|
|
3449
|
+
);
|
|
3450
|
+
console.log(
|
|
3451
|
+
" - Or make it your default wallet: zero wallet set <privateKey> --force"
|
|
3452
|
+
);
|
|
3453
|
+
console.log("");
|
|
3454
|
+
}
|
|
3455
|
+
analyticsService.capture("wallet_generated", {
|
|
3456
|
+
format: options.json ? "json" : "text",
|
|
3457
|
+
funded: options.fund ?? false,
|
|
3458
|
+
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3459
|
+
funding_url_minted: options.fund ? fundingUrl !== null : false
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
);
|
|
3233
3463
|
var walletCommand = (appContext) => {
|
|
3234
3464
|
const cmd = new Command10("wallet").description("Manage your wallet");
|
|
3235
3465
|
cmd.addCommand(walletBalanceCommand(appContext));
|
|
3236
3466
|
cmd.addCommand(walletFundCommand(appContext));
|
|
3237
3467
|
cmd.addCommand(walletAddressCommand(appContext));
|
|
3238
3468
|
cmd.addCommand(walletSetCommand(appContext));
|
|
3469
|
+
cmd.addCommand(walletGenerateCommand(appContext));
|
|
3239
3470
|
return cmd;
|
|
3240
3471
|
};
|
|
3241
3472
|
|
|
@@ -3260,9 +3491,37 @@ var readPrivateKey = () => {
|
|
|
3260
3491
|
}
|
|
3261
3492
|
return null;
|
|
3262
3493
|
};
|
|
3494
|
+
var linuxInstallHint = () => {
|
|
3495
|
+
return [
|
|
3496
|
+
" On Debian/Ubuntu: sudo apt install xdg-utils",
|
|
3497
|
+
" On RHEL/Fedora: sudo dnf install xdg-utils"
|
|
3498
|
+
].join("\n");
|
|
3499
|
+
};
|
|
3500
|
+
var printManualFallback = (url) => {
|
|
3501
|
+
const lines = [
|
|
3502
|
+
"",
|
|
3503
|
+
"\u26A0\uFE0F Couldn't auto-open your browser (this is common on WSL, SSH, or",
|
|
3504
|
+
" containers \u2014 no xdg-open installed).",
|
|
3505
|
+
"",
|
|
3506
|
+
"Copy and paste this URL into your browser to claim:",
|
|
3507
|
+
"",
|
|
3508
|
+
` ${url}`,
|
|
3509
|
+
""
|
|
3510
|
+
];
|
|
3511
|
+
if (process.platform === "linux") {
|
|
3512
|
+
lines.push(
|
|
3513
|
+
"Tip: install xdg-utils so 'zero welcome' can open the browser for you:",
|
|
3514
|
+
linuxInstallHint(),
|
|
3515
|
+
""
|
|
3516
|
+
);
|
|
3517
|
+
}
|
|
3518
|
+
console.log(lines.join("\n"));
|
|
3519
|
+
};
|
|
3263
3520
|
var welcomeCommand = (appContext) => new Command11("welcome").description("Claim your $5 welcome bonus.").action(async () => {
|
|
3264
3521
|
const { analyticsService } = appContext.services;
|
|
3265
3522
|
analyticsService.capture("welcome_started", {});
|
|
3523
|
+
let walletAddress;
|
|
3524
|
+
let url;
|
|
3266
3525
|
try {
|
|
3267
3526
|
let privateKey = readPrivateKey();
|
|
3268
3527
|
if (!privateKey) {
|
|
@@ -3273,30 +3532,45 @@ var welcomeCommand = (appContext) => new Command11("welcome").description("Claim
|
|
|
3273
3532
|
}
|
|
3274
3533
|
}
|
|
3275
3534
|
const account = privateKeyToAccount3(privateKey);
|
|
3276
|
-
|
|
3535
|
+
walletAddress = getAddress(account.address);
|
|
3277
3536
|
const walletSignature = await account.signMessage({
|
|
3278
3537
|
message: walletAddress
|
|
3279
3538
|
});
|
|
3280
|
-
|
|
3539
|
+
url = new URL("/welcome", appContext.env.ZERO_WEB_URL);
|
|
3281
3540
|
url.searchParams.set("wallet", walletAddress);
|
|
3282
3541
|
url.searchParams.set("walletSignature", walletSignature);
|
|
3542
|
+
} catch (err) {
|
|
3543
|
+
analyticsService.capture("welcome_failed", {
|
|
3544
|
+
error: truncateError(
|
|
3545
|
+
err instanceof Error ? err.message : String(err)
|
|
3546
|
+
)
|
|
3547
|
+
});
|
|
3548
|
+
throw err;
|
|
3549
|
+
}
|
|
3550
|
+
const urlString = url.toString();
|
|
3551
|
+
analyticsService.setWalletAddress(walletAddress);
|
|
3552
|
+
try {
|
|
3553
|
+
await open2(urlString);
|
|
3283
3554
|
console.log(
|
|
3284
|
-
`Opening ${
|
|
3555
|
+
`Opening ${urlString}
|
|
3285
3556
|
|
|
3286
3557
|
If your browser didn't open, paste the URL above.`
|
|
3287
3558
|
);
|
|
3288
|
-
await open2(url.toString());
|
|
3289
3559
|
analyticsService.capture("welcome_link_opened", {
|
|
3290
3560
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3291
|
-
|
|
3561
|
+
open_method: "auto"
|
|
3292
3562
|
});
|
|
3293
3563
|
} catch (err) {
|
|
3294
|
-
|
|
3295
|
-
|
|
3564
|
+
printManualFallback(urlString);
|
|
3565
|
+
analyticsService.capture("welcome_link_opened", {
|
|
3566
|
+
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3567
|
+
open_method: "manual_copy",
|
|
3568
|
+
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3569
|
+
open_error: truncateError(
|
|
3296
3570
|
err instanceof Error ? err.message : String(err)
|
|
3297
|
-
)
|
|
3571
|
+
),
|
|
3572
|
+
platform: process.platform
|
|
3298
3573
|
});
|
|
3299
|
-
throw err;
|
|
3300
3574
|
}
|
|
3301
3575
|
});
|
|
3302
3576
|
|
|
@@ -3467,12 +3741,30 @@ var AnalyticsService = class {
|
|
|
3467
3741
|
setAgentHost(next) {
|
|
3468
3742
|
this.agentHost = next;
|
|
3469
3743
|
}
|
|
3744
|
+
// Per-invocation override for the `wallet_address` super-prop. Used by
|
|
3745
|
+
// commands that mint or load a wallet mid-process (init, wallet set,
|
|
3746
|
+
// welcome) so subsequent captures carry the now-known address without
|
|
3747
|
+
// needing to pass it as a per-event prop (which would collide with the
|
|
3748
|
+
// super-prop — see ZERO-43). distinctId is intentionally NOT changed:
|
|
3749
|
+
// the anon→wallet identity merge happens on the next CLI invocation
|
|
3750
|
+
// via PostHog alias, when AnalyticsService is constructed with both
|
|
3751
|
+
// the persisted anonId and the new walletAddress.
|
|
3752
|
+
setWalletAddress(walletAddress) {
|
|
3753
|
+
this.walletAddress = walletAddress;
|
|
3754
|
+
}
|
|
3470
3755
|
capture(event, properties) {
|
|
3471
3756
|
if (!this.posthog) return;
|
|
3472
3757
|
this.posthog.capture({
|
|
3473
3758
|
distinctId: this.distinctId,
|
|
3474
3759
|
event,
|
|
3760
|
+
// Super-props are spread LAST so they always win against a same-
|
|
3761
|
+
// named per-event prop (see ZERO-43: a `source: undefined` on
|
|
3762
|
+
// `search_executed` was silently clobbering the `cli` surface stamp).
|
|
3763
|
+
// The reserved super-prop names are enforced by the events registry
|
|
3764
|
+
// test in `analytics/__tests__/events.test.ts` — any new event prop
|
|
3765
|
+
// that collides will fail CI.
|
|
3475
3766
|
properties: {
|
|
3767
|
+
...properties,
|
|
3476
3768
|
source: "cli",
|
|
3477
3769
|
// biome-ignore lint/style/useNamingConvention: snake_case is standard for analytics event properties
|
|
3478
3770
|
cli_version: this.cliVersion,
|
|
@@ -3482,14 +3774,14 @@ var AnalyticsService = class {
|
|
|
3482
3774
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3483
3775
|
request_id: this.requestId,
|
|
3484
3776
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3485
|
-
agent_host: this.agentHost
|
|
3486
|
-
...properties
|
|
3777
|
+
agent_host: this.agentHost
|
|
3487
3778
|
}
|
|
3488
3779
|
});
|
|
3489
3780
|
}
|
|
3490
3781
|
captureException(error, properties) {
|
|
3491
3782
|
if (!this.posthog) return;
|
|
3492
3783
|
this.posthog.captureException(error, this.distinctId, {
|
|
3784
|
+
...properties,
|
|
3493
3785
|
source: "cli",
|
|
3494
3786
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3495
3787
|
cli_version: this.cliVersion,
|
|
@@ -3499,8 +3791,7 @@ var AnalyticsService = class {
|
|
|
3499
3791
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3500
3792
|
request_id: this.requestId,
|
|
3501
3793
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
3502
|
-
agent_host: this.agentHost
|
|
3503
|
-
...properties
|
|
3794
|
+
agent_host: this.agentHost
|
|
3504
3795
|
});
|
|
3505
3796
|
}
|
|
3506
3797
|
async shutdown() {
|
|
@@ -3560,18 +3851,23 @@ var StateService = class {
|
|
|
3560
3851
|
}
|
|
3561
3852
|
};
|
|
3562
3853
|
// Walk recent searches newest-first, returning the first one whose
|
|
3563
|
-
// results contain `
|
|
3564
|
-
// "loadLastSearch" for attributing rank — handles the
|
|
3565
|
-
// case where the most recent search isn't the one being
|
|
3566
|
-
|
|
3854
|
+
// results contain `capabilityRef` (matches against uid OR slug).
|
|
3855
|
+
// Preferred over "loadLastSearch" for attributing rank — handles the
|
|
3856
|
+
// parallel-search case where the most recent search isn't the one being
|
|
3857
|
+
// fetched.
|
|
3858
|
+
findSearchContextByCapability = (capabilityRef) => {
|
|
3567
3859
|
const recent = this.loadRecentSearches();
|
|
3568
3860
|
for (const search of recent.searches) {
|
|
3569
|
-
const entry = search.capabilities.find(
|
|
3861
|
+
const entry = search.capabilities.find(
|
|
3862
|
+
(c) => c.id === capabilityRef || c.slug === capabilityRef
|
|
3863
|
+
);
|
|
3570
3864
|
if (entry) {
|
|
3571
3865
|
return {
|
|
3572
3866
|
searchId: search.searchId,
|
|
3573
3867
|
resultRank: entry.position,
|
|
3574
3868
|
capabilityId: entry.id,
|
|
3869
|
+
capabilityUid: entry.id,
|
|
3870
|
+
capabilitySlug: entry.slug ?? null,
|
|
3575
3871
|
url: entry.url,
|
|
3576
3872
|
displayCostAmount: entry.displayCostAmount
|
|
3577
3873
|
};
|
|
@@ -3596,6 +3892,8 @@ var StateService = class {
|
|
|
3596
3892
|
searchId: search.searchId,
|
|
3597
3893
|
resultRank: entry.position,
|
|
3598
3894
|
capabilityId: entry.id,
|
|
3895
|
+
capabilityUid: entry.id,
|
|
3896
|
+
capabilitySlug: entry.slug ?? null,
|
|
3599
3897
|
url: entry.url,
|
|
3600
3898
|
displayCostAmount: entry.displayCostAmount
|
|
3601
3899
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeroxyz/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"zero": "dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "tsup src/index.ts --format esm --out-dir dist --clean",
|
|
19
|
-
"build:binary": "tsup --config tsup.binary.ts && cp -r skills hooks dist/pkg/ && pnpm exec pkg dist/pkg/index.cjs --config pkg.json --targets node24-macos-arm64,node24-macos-x64,node24-linux-x64 --output dist/bin/zero",
|
|
19
|
+
"build:binary": "tsup --config tsup.binary.ts && cp -r skills hooks dist/pkg/ && pnpm exec pkg dist/pkg/index.cjs --config pkg.json --targets node24-macos-arm64,node24-macos-x64,node24-linux-x64,node24-linux-arm64 --output dist/bin/zero",
|
|
20
20
|
"prepublishOnly": "pnpm run build",
|
|
21
21
|
"dev": "ZERO_ENV=development tsx src/index.ts",
|
|
22
22
|
"cli": "ZERO_ENV=development ZERO_API_URL=http://localhost:1111 tsx src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@x402/extensions": "^2.9.0",
|
|
35
35
|
"@x402/fetch": "^2.9.0",
|
|
36
36
|
"commander": "^13.0.0",
|
|
37
|
-
"mppx": "^0.
|
|
37
|
+
"mppx": "^0.6.9",
|
|
38
38
|
"open": "^11.0.0",
|
|
39
39
|
"posthog-node": "^5.29.2",
|
|
40
40
|
"viem": "^2.47.10",
|
package/skills/zero/SKILL.md
CHANGED
|
@@ -98,13 +98,16 @@ zero review --capability <slug> --success --accuracy <1-5> --value <1-5> --relia
|
|
|
98
98
|
|
|
99
99
|
### Workflow
|
|
100
100
|
|
|
101
|
-
1. **Search** — `zero search "weather forecast"` finds matching capabilities. Results show name, cost,
|
|
101
|
+
1. **Search** — `zero search "weather forecast"` finds matching capabilities. Results show ranked capabilities with name, cost, availability, and a short description.
|
|
102
102
|
> **Detail-page → CLI bridge.** When you only have a capability slug (e.g. copied from a zero.xyz capability page), `zero get <slug>` and `zero fetch --capability <slug>` both work as drop-in replacements for the position-based forms. You don't need to run `zero search` first.
|
|
103
103
|
|
|
104
104
|
2. **Inspect** — `zero get 1 --formatted` prints a human summary **and a copy-pasteable `Try it:` command** wired to the capability's schema. Plain `zero get 1` returns full JSON (URL, method, `bodySchema`, examples, pricing) for `jq` pipelines. **If `bodySchema` is `null`**, the capability hasn't been schema-indexed yet — skip it and `zero get 2`, don't invent field names.
|
|
105
105
|
3. **Call** — `zero fetch <url>` makes the request. If the server returns 402, payment is handled automatically (x402 and MPP, including cross-chain bridging from Base to Tempo).
|
|
106
106
|
4. **Review** — `zero review <runId>` submits a quality review. Run IDs are printed to **stderr** after a successful fetch (or returned on stdout in `--json` mode). Always review after a paid call, and **pass `--content "<notes>"` whenever you have something specific to say** — the content line lands on the capability's public detail page on zero.xyz, so it's what the next human buyer (and the next agent) reads when deciding whether to call this capability. See "Writing review content" below.
|
|
107
107
|
5. **Retroactive review** — if you lost a runId, run `zero runs --unreviewed` (or `zero runs --capability <slug> --unreviewed`). `zero review --capability <slug> ...` auto-resolves to your most recent un-reviewed run for that capability.
|
|
108
|
+
6. **Revise a review** — re-running `zero review <runId> ...` for a run you've already reviewed **overwrites** your prior ratings and content (same wallet only). Use this when a retry succeeded, a result that looked good failed downstream, or you want to expand the notes after more usage. There's no separate edit command — same call, same runId.
|
|
109
|
+
|
|
110
|
+
> **Only your latest review per capability is shown publicly.** When you've reviewed a capability across multiple runs, both the public detail page and the search-ranking math dedupe to your most recent review per capability — older reviews stay in the DB as a version trail but don't display and don't count toward the rating. So if your judgment has changed, submit a fresh review on a recent run (or edit the existing one via the same-`runId` upsert above) — don't assume the old review still represents you.
|
|
108
111
|
|
|
109
112
|
### Writing review content
|
|
110
113
|
|
|
@@ -118,6 +121,10 @@ zero review --capability <slug> --success --accuracy <1-5> --value <1-5> --relia
|
|
|
118
121
|
|
|
119
122
|
Each names the task attempted, what the output actually was, and a specific observation (latency, a gotcha, a fit/misfit note). That's the kind of line a human buyer trusts and another agent can learn from.
|
|
120
123
|
|
|
124
|
+
**Name the use case.** State the general type of work you were using the capability for (no private/proprietary detail) so other readers can tell whether their task fits. Examples: *"Used this to generate stock-style hero photography for a vibe-coded landing page"*, *"Called from a daily ETL to enrich new signups with company metadata"*, *"Translating short product blurbs (~80 chars) for a Spanish-language store"*. This is what makes a review useful for someone deciding whether to call the capability for *their* task — not just whether it worked for yours.
|
|
125
|
+
|
|
126
|
+
**Two-part structure works well** when you have enough to say: lead with the human-facing half (use case, what you got, fit/misfit), then a second half with agent-facing technical notes (exact field names, gotchas, retry behavior, schema quirks). Each half is independently useful — humans skim the top, agents grep the bottom.
|
|
127
|
+
|
|
121
128
|
**Review failures with content too.** Failure notes are arguably more valuable — they warn the next caller. Example: *"FLUX Schnell returned HTTP 500 Internal Server Error — paid 0.003 USDC via MPP but got no image."* Pair with `--no-success`.
|
|
122
129
|
|
|
123
130
|
**Skip `--content` rather than write filler.** "Worked great", "Fast response", or test strings like "trial 1" add noise, pollute the capability's public page, and dilute the signal agents rely on. If you don't have a specific observation, just submit the numeric ratings.
|
|
@@ -231,7 +238,7 @@ cat payload.json | zero fetch https://api.example.com --data-stdin
|
|
|
231
238
|
|
|
232
239
|
### Rules
|
|
233
240
|
|
|
234
|
-
- **Always `zero search` fresh, every time.** Never reuse a capability URL, slug, schema, or price from an earlier turn, prior conversation, training data, or memory. Capabilities churn constantly — endpoints go offline, prices change, schemas evolve, and rankings shift as reviews accumulate. A capability that worked yesterday may be dead, repriced, or outranked today. Searching again costs nothing and is the only way to get current
|
|
241
|
+
- **Always `zero search` fresh, every time.** Never reuse a capability URL, slug, schema, or price from an earlier turn, prior conversation, training data, or memory. Capabilities churn constantly — endpoints go offline, prices change, schemas evolve, and rankings shift as reviews accumulate. A capability that worked yesterday may be dead, repriced, or outranked today. Searching again costs nothing and is the only way to get current ranking and availability.
|
|
235
242
|
- **Always `zero get` before `zero fetch`.** Even if you "know" the URL, re-fetch the full details to confirm the URL, method, required headers, body schema, and current price. Do not reconstruct a fetch call from memory.
|
|
236
243
|
- Never guess endpoint URLs or schemas.
|
|
237
244
|
- Use `--max-pay` before potentially expensive requests.
|