From 6ab6c681c2f8e5dd7d32b8112c1f4ae6f8193eda Mon Sep 17 00:00:00 2001 From: Karen Sheng Date: Fri, 4 Sep 2026 00:22:45 -0700 Subject: [PATCH 1/2] feat(run): pass --docs-check to selat-pay for --allow-unlisted pins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unlisted pins have no catalog terms and no vetting — the merchant's own published docs (llms.txt / openapi.json) are the only request contract there is, and merchants in this class can capture payment BEFORE validating the body (observed: api.auto.exchange, three charges settled on Tempo then 400'd on 'input' vs the documented required 'prompt', 2026-09-04). With --docs-check, selat-pay resolves those docs pre-sign and refuses a body missing a documented-required field. Ships with the selat-pay release that adds the flag (older selat-pay rejects unknown flags) — land after that release and bump the dep pin in the same change. Co-Authored-By: Claude Fable 5 --- lib/commands/run.mjs | 20 +++++++++++++++++++- test/run-allow-unlisted.test.mjs | 14 +++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/commands/run.mjs b/lib/commands/run.mjs index 87d8970..3481870 100644 --- a/lib/commands/run.mjs +++ b/lib/commands/run.mjs @@ -182,6 +182,19 @@ export function rankPickArgv({ intent, liveProbe = false, endpoint, method, capa ]; } +// Unlisted pins have no catalog terms and no vetting — the merchant's own +// published docs (llms.txt / openapi.json) are the only request contract +// there is, and merchants in this class can capture payment BEFORE validating +// the body (observed: api.auto.exchange, 3 charges settled then 400'd on a +// wrong param name, 2026-09-04). --docs-check makes selat-pay resolve those +// docs pre-sign and refuse a body missing a documented-required field. +// Requires selat-pay >= 0.10 (older versions reject the flag as unknown — +// the dep pin guarantees the pair). +export function withDocsCheck(selatPayArgs, { allowUnlisted = false } = {}) { + if (!allowUnlisted || selatPayArgs.includes("--docs-check")) return selatPayArgs; + return [...selatPayArgs, "--docs-check"]; +} + /** * Map rank.mjs's pin-specific exit codes to an honest error. * @@ -261,7 +274,10 @@ export async function run(args) { console.log(" the only source of price and rail, and the default cap"); console.log(" is the flat no-price $0.10 (raise with --max-amount; the"); console.log(" $1 hard ceiling, validate-before-sign, and Circle policy"); - console.log(" caps still apply). Explicit per call, never implied."); + console.log(" caps still apply). Also resolves the merchant's published"); + console.log(" docs (llms.txt / openapi.json) as a request contract: a"); + console.log(" body missing a documented-required field is refused"); + console.log(" before signing. Explicit per call, never implied."); console.log(" --capability Rank only endpoints labeled with this capability"); console.log(" (Layer 0). Unknown names and empty labeled pools"); console.log(" are refused, never silently widened."); @@ -447,6 +463,8 @@ export async function run(args) { return emitRunError({ jsonMode, error: capped.reason }); } selatPayArgs = capped.args; + + selatPayArgs = withDocsCheck(selatPayArgs, { allowUnlisted }); display = ["selat-pay", ...selatPayArgs].map(shellQuoteForDisplay).join(" "); // --dry-run stops HERE: the pick is ranked and the selat-pay command is diff --git a/test/run-allow-unlisted.test.mjs b/test/run-allow-unlisted.test.mjs index fcbb282..1e8beca 100644 --- a/test/run-allow-unlisted.test.mjs +++ b/test/run-allow-unlisted.test.mjs @@ -5,7 +5,7 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { parseRunArgs, rankPickArgv, KNOWN_RUN_FLAGS, pinRefusal } from "../lib/commands/run.mjs"; +import { parseRunArgs, rankPickArgv, KNOWN_RUN_FLAGS, pinRefusal, withDocsCheck } from "../lib/commands/run.mjs"; test("--allow-unlisted is a known flag and parses with --endpoint", () => { assert.ok(KNOWN_RUN_FLAGS.includes("--allow-unlisted")); @@ -35,6 +35,18 @@ test("rankPickArgv forwards --allow-unlisted only alongside a pin", () => { ); }); +// The merchant's published docs are the only request contract an unlisted +// pin has — the paid command must carry --docs-check so selat-pay refuses a +// body missing a documented-required field before signing. +test("withDocsCheck appends --docs-check for unlisted pins only, idempotently", () => { + const base = ["POST", "https://api.x.dev/v1", "--chain", "base", "--max-amount", "0.1"]; + assert.deepEqual(withDocsCheck(base, { allowUnlisted: true }), [...base, "--docs-check"]); + assert.deepEqual(withDocsCheck(base, { allowUnlisted: false }), base); + assert.deepEqual(withDocsCheck(base, {}), base); + const already = [...base, "--docs-check"]; + assert.deepEqual(withDocsCheck(already, { allowUnlisted: true }), already); +}); + test("the not-in-catalog refusal names the flag", () => { const refusal = pinRefusal(4, "https://api.x.dev/v1"); assert.equal(refusal.reason, "endpoint-not-in-catalog"); From 45077ba7019dfb4d804ba4c4a54d3b51beb44289 Mon Sep 17 00:00:00 2001 From: Karen Sheng Date: Fri, 4 Sep 2026 00:50:17 -0700 Subject: [PATCH 2/2] chore(deps): bump @selat-ai/selat-pay to ^0.10.0 (--docs-check floor) The run command now passes --docs-check for --allow-unlisted pins; selat-pay <=0.9.14 rejects unknown flags, so this pin is the compatibility floor. Co-Authored-By: Claude Fable 5 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 108c95b..f9e8d9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@selat-ai/selat-discovery": "^0.24.9", - "@selat-ai/selat-pay": "^0.9.14", + "@selat-ai/selat-pay": "^0.10.0", "qrcode-generator": "2.0.4" }, "bin": { @@ -142,9 +142,9 @@ } }, "node_modules/@selat-ai/selat-pay": { - "version": "0.9.14", - "resolved": "https://registry.npmjs.org/@selat-ai/selat-pay/-/selat-pay-0.9.14.tgz", - "integrity": "sha512-bGnae/zRq3wy+8ZspVxPpq6qxjuC4w0F5+YxL5P75aTuREseUdO2zWd6wjlbEzisfg3whmVVc8fXuXOf2Z1Omw==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@selat-ai/selat-pay/-/selat-pay-0.10.0.tgz", + "integrity": "sha512-rwCASrbyPdOefX99QqFdI1HjCMngpk7uA6BRlSnMz2cSdQMuLpB0MhcAsY+kPY601NHQOKHQhATkeBOZABLSLQ==", "license": "Apache-2.0", "dependencies": { "@circle-fin/x402-batching": "^3.1.2", diff --git a/package.json b/package.json index fefddc6..1e81ad1 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@selat-ai/selat-discovery": "^0.24.9", - "@selat-ai/selat-pay": "^0.9.14", + "@selat-ai/selat-pay": "^0.10.0", "qrcode-generator": "2.0.4" }, "overrides": {