@naulon/wayfarer 0.1.1

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.
Files changed (86) hide show
  1. package/README.md +39 -0
  2. package/dist/agent.d.ts +77 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +235 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/allocation.d.ts +36 -0
  7. package/dist/allocation.d.ts.map +1 -0
  8. package/dist/allocation.js +45 -0
  9. package/dist/allocation.js.map +1 -0
  10. package/dist/appraise.d.ts +3 -0
  11. package/dist/appraise.d.ts.map +1 -0
  12. package/dist/appraise.js +60 -0
  13. package/dist/appraise.js.map +1 -0
  14. package/dist/buyer.d.ts +195 -0
  15. package/dist/buyer.d.ts.map +1 -0
  16. package/dist/buyer.js +254 -0
  17. package/dist/buyer.js.map +1 -0
  18. package/dist/decide.d.ts +115 -0
  19. package/dist/decide.d.ts.map +1 -0
  20. package/dist/decide.js +206 -0
  21. package/dist/decide.js.map +1 -0
  22. package/dist/discover.d.ts +10 -0
  23. package/dist/discover.d.ts.map +1 -0
  24. package/dist/discover.js +5 -0
  25. package/dist/discover.js.map +1 -0
  26. package/dist/discovery.d.ts +26 -0
  27. package/dist/discovery.d.ts.map +1 -0
  28. package/dist/discovery.js +93 -0
  29. package/dist/discovery.js.map +1 -0
  30. package/dist/gateway.d.ts +117 -0
  31. package/dist/gateway.d.ts.map +1 -0
  32. package/dist/gateway.js +187 -0
  33. package/dist/gateway.js.map +1 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +19 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/lib.d.ts +40 -0
  39. package/dist/lib.d.ts.map +1 -0
  40. package/dist/lib.js +44 -0
  41. package/dist/lib.js.map +1 -0
  42. package/dist/licenseStore.d.ts +56 -0
  43. package/dist/licenseStore.d.ts.map +1 -0
  44. package/dist/licenseStore.js +79 -0
  45. package/dist/licenseStore.js.map +1 -0
  46. package/dist/memo.d.ts +43 -0
  47. package/dist/memo.d.ts.map +1 -0
  48. package/dist/memo.js +102 -0
  49. package/dist/memo.js.map +1 -0
  50. package/dist/origin-policy.d.ts +74 -0
  51. package/dist/origin-policy.d.ts.map +1 -0
  52. package/dist/origin-policy.js +88 -0
  53. package/dist/origin-policy.js.map +1 -0
  54. package/dist/paidFetch.d.ts +14 -0
  55. package/dist/paidFetch.d.ts.map +1 -0
  56. package/dist/paidFetch.js +93 -0
  57. package/dist/paidFetch.js.map +1 -0
  58. package/dist/pay.d.ts +3 -0
  59. package/dist/pay.d.ts.map +1 -0
  60. package/dist/pay.js +82 -0
  61. package/dist/pay.js.map +1 -0
  62. package/dist/pop.d.ts +8 -0
  63. package/dist/pop.d.ts.map +1 -0
  64. package/dist/pop.js +25 -0
  65. package/dist/pop.js.map +1 -0
  66. package/dist/rail.d.ts +18 -0
  67. package/dist/rail.d.ts.map +1 -0
  68. package/dist/rail.js +73 -0
  69. package/dist/rail.js.map +1 -0
  70. package/dist/rss.d.ts +38 -0
  71. package/dist/rss.d.ts.map +1 -0
  72. package/dist/rss.js +110 -0
  73. package/dist/rss.js.map +1 -0
  74. package/dist/sign.d.ts +13 -0
  75. package/dist/sign.d.ts.map +1 -0
  76. package/dist/sign.js +52 -0
  77. package/dist/sign.js.map +1 -0
  78. package/dist/types.d.ts +75 -0
  79. package/dist/types.d.ts.map +1 -0
  80. package/dist/types.js +2 -0
  81. package/dist/types.js.map +1 -0
  82. package/dist/wallet.d.ts +12 -0
  83. package/dist/wallet.d.ts.map +1 -0
  84. package/dist/wallet.js +46 -0
  85. package/dist/wallet.js.map +1 -0
  86. package/package.json +42 -0
@@ -0,0 +1,195 @@
1
+ declare const AGENT_UA = "naulon-wayfarer/0.1";
2
+ export interface Quoted {
3
+ priceUsdc: number;
4
+ amountAtomic: string;
5
+ /** Nonce the gate issued on the 402; echo it back in the payment (replay guard). */
6
+ nonce?: string;
7
+ requirements: {
8
+ network: string;
9
+ asset: string;
10
+ payTo: string;
11
+ amount: string;
12
+ maxTimeoutSeconds: number;
13
+ };
14
+ /**
15
+ * When the publisher declares extra settlement legs (e.g. a control-plane operator
16
+ * fee), the gate advertises the FULL per-leg list (author first) via the
17
+ * `extensions.naulonLegs` block, and rejects any payment that doesn't sign every leg.
18
+ * Present only for such an N-leg quote; absent for the stock single-author toll —
19
+ * then the buyer pays the bare single payload, byte-identical to before.
20
+ */
21
+ legs?: {
22
+ role: string;
23
+ payTo: string;
24
+ amount: string;
25
+ nonce?: string;
26
+ }[];
27
+ /** The 402's `resource` object (`{url, description, mimeType}`), verbatim. The gateway
28
+ * rail's payment envelope MUST echo it back — the facilitator's `verify` rejects a
29
+ * payload missing `resource` (400 `resource: Required`). Absent on the memo rail, which
30
+ * relays the raw authorization and never sends the x402 envelope. */
31
+ resource?: unknown;
32
+ }
33
+ /** A per-leg requirement the buyer signs — the author requirement with this leg's
34
+ * payTo + amount substituted (same network/asset/timeout). */
35
+ export type LegRequirements = Quoted["requirements"];
36
+ /**
37
+ * Why a paid fetch failed, classified so the host can decide what to do (BUY-1.4).
38
+ * The point is the `retryable` split: `insufficient_funds` is a HARD stop (fund the
39
+ * wallet, don't re-call), while `toll_moved` / `expired` / `rejected` are transient
40
+ * (re-quote and try again may succeed). `not_gated` means there was nothing to pay.
41
+ */
42
+ export type FetchErrorCode = "not_gated" | "not_found" | "toll_moved" | "insufficient_funds" | "expired" | "rejected" | "origin_error" | "needs_topup" | "grant_expired" | "settlement_ambiguous";
43
+ export interface Fetched {
44
+ ok: boolean;
45
+ content?: string;
46
+ settlementRef?: string;
47
+ /** The author-leg amount paid, in USDC (back-compat — only the primary leg). */
48
+ paidUsdc?: number;
49
+ /** The buyer's TRUE outflow authorized for this read, in USDC — the sum across every
50
+ * settlement leg (== quotedTotalAtomic of the quote actually signed). Callers debit
51
+ * budgets on THIS, not paidUsdc, so a fee'd toll is never under-counted. */
52
+ costUsdc?: number;
53
+ /** Citation License (compact JWS) the gate handed back on a paid read. */
54
+ license?: string;
55
+ error?: string;
56
+ /** Typed failure classification (BUY-1.4); absent on success. */
57
+ errorCode?: FetchErrorCode;
58
+ /** True when re-quoting/retrying may succeed (toll moved, validity expired, a
59
+ * generic rejection); false for a hard stop (insufficient funds — fund first).
60
+ * Absent on success. */
61
+ retryable?: boolean;
62
+ }
63
+ /**
64
+ * A pay-time spend ceiling the buyer must not exceed (BUY-1.4). The buyer re-quotes
65
+ * at pay time (its own pre-pay probe IS the re-quote) and ABORTS — paying nothing —
66
+ * if the live toll total tops this. The caller (the MCP) sets it to the quote it
67
+ * already gated the budget on, plus a configured tolerance, so a toll that moved up
68
+ * between the budget check and the pay can never silently overspend.
69
+ */
70
+ export interface PayGuard {
71
+ /** Max atomic (micro-USDC) total across all legs the buyer may pay. */
72
+ maxTotalAtomic: string;
73
+ }
74
+ export interface Buyer {
75
+ readonly address: string;
76
+ /** One-time setup (gateway mode deposits USDC into the Gateway Wallet). */
77
+ init(): Promise<void>;
78
+ /** Probe price without paying. null if the article isn't gated. */
79
+ price(url: string, kind: "read" | "citation"): Promise<Quoted | null>;
80
+ /** Pay and fetch the content. `guard` (optional) caps the pay-time total — the
81
+ * buyer aborts beyond it (toll-moved protection), paying nothing. */
82
+ fetch(url: string, kind: "read" | "citation", guard?: PayGuard): Promise<Fetched>;
83
+ }
84
+ /**
85
+ * The buyer's true outflow for a quote, in atomic micro-USDC (integer) — the sum of
86
+ * every advertised settlement leg, or the single author amount when there are none.
87
+ * Integer math only (AGENTS.md: money is integer micro-USDC, never floats).
88
+ */
89
+ export declare function quotedTotalAtomic(quoted: Quoted): bigint;
90
+ /**
91
+ * The toll-moved guard. If `guard` is set and the LIVE quote's true total tops the
92
+ * guard ceiling, returns a typed `toll_moved` failure (the buyer pays NOTHING);
93
+ * otherwise null (clear to pay). This is "re-quote at pay time, abort beyond
94
+ * tolerance": the buyer's own pre-pay probe is the re-quote, compared here against
95
+ * the ceiling the caller authorized.
96
+ */
97
+ export declare function tollMovedOrNull(quoted: Quoted, guard?: PayGuard): Fetched | null;
98
+ /**
99
+ * Classify a gate's payment-rejection message (BUY-1.4). The gate surfaces the real
100
+ * reason in its 402 body; this maps the known signals onto a typed code + a retry
101
+ * verdict. Insufficient funds is the one hard stop — every other rejection is worth a
102
+ * re-quote. Conservative by design: an unrecognized reason is `rejected` (retryable),
103
+ * never silently treated as a fundable hard stop.
104
+ */
105
+ /**
106
+ * Classify a THROWN session-signer refusal (BUY-4 hosted path). The cloud injects a
107
+ * grant-guarded session signer whose `signTypedData` throws an Error whose message is the
108
+ * sign guard's code, optionally suffixed " (remaining <micro>)". Unlike `classifyPaymentError`
109
+ * (which reads a gate 402 body), this refusal never reached the gate — nothing was paid — and
110
+ * must NOT be run through the 402 classifier, where `grant_exceeded`/`no_session` would fall
111
+ * through to a retryable `rejected`, telling the agent to retry a pay that can only fail again.
112
+ *
113
+ * grant_exceeded · leg_too_large · no_session → needs_topup (fund the session)
114
+ * grant_expired → grant_expired (renew — funds intact)
115
+ * bad_from · chain_mismatch · payee_not_allowed → rejected (a config error a top-up can't fix)
116
+ *
117
+ * Returns null when the message is NOT a known signer code (a real socket / unknown throw the
118
+ * caller should surface as `origin_error`). Never retryable: every signer refusal needs a
119
+ * host-side action (fund / renew / fix config), never a blind re-call.
120
+ */
121
+ export declare function classifySignerRefusal(errorText: string): {
122
+ errorCode: FetchErrorCode;
123
+ retryable: boolean;
124
+ } | null;
125
+ export declare function classifyPaymentError(errorText: string): {
126
+ errorCode: FetchErrorCode;
127
+ retryable: boolean;
128
+ };
129
+ /**
130
+ * The classified outcome of a price probe. A probe is NOT just "gated or not" — the
131
+ * reason it isn't gated matters, and collapsing everything non-402 into one bucket is a
132
+ * money-correctness bug: a wrong URL (404) or a down origin (5xx) is NOT a free read, and
133
+ * an agent that treats it as one silently skips paying and reads nothing (or an error
134
+ * page). This union keeps the four cases apart so every caller can respond correctly:
135
+ * - `gated` — a real 402 with a decodable toll; pay it.
136
+ * - `free` — a genuine 2xx; there is nothing to pay (the one true "not gated").
137
+ * - `not_found` — a 404; the path is wrong/unknown, NOT free. On a slug-only pay this
138
+ * is usually the `/essays/<slug>` fallback missing a publisher that
139
+ * serves `/articles/<slug>` — the fix is to pass the canonical url.
140
+ * - `unreachable` — any other non-2xx (5xx, 403, a network throw); transient, retryable.
141
+ * - `malformed` — a 402 whose PAYMENT-REQUIRED header is missing/undecodable/empty;
142
+ * a broken gate, never silently a free read.
143
+ */
144
+ export type ProbeOutcome = {
145
+ status: "gated";
146
+ quoted: Quoted;
147
+ } | {
148
+ status: "free";
149
+ } | {
150
+ status: "not_found";
151
+ httpStatus: number;
152
+ } | {
153
+ status: "unreachable";
154
+ httpStatus: number;
155
+ } | {
156
+ status: "malformed";
157
+ reason: string;
158
+ };
159
+ /** Shared price probe — classify the gate's response by HTTP status, decoding the 402
160
+ * PAYMENT-REQUIRED header only for a real toll. Never throws: a broken 402 body or a
161
+ * network failure is returned as a typed outcome, not an exception. */
162
+ export declare function probe(url: string, kind: "read" | "citation", agentId: string): Promise<ProbeOutcome>;
163
+ /** Back-compat thin wrapper: the decoded quote for a gated 402, else null. Callers that
164
+ * must distinguish free / not_found / unreachable use `probe()` directly. */
165
+ export declare function probePrice(url: string, kind: "read" | "citation", agentId: string): Promise<Quoted | null>;
166
+ /**
167
+ * Map a NON-gated probe outcome to the typed `Fetched` failure every buyer returns, so a
168
+ * 404/5xx/malformed response never masquerades as a paid or free success. `not_gated` is
169
+ * reserved for the one true free (2xx) read; a 404 is `not_found` with a message that
170
+ * points the agent at the canonical url (the usual cause is the `/essays/<slug>` fallback
171
+ * not matching a `/articles/<slug>` publisher).
172
+ */
173
+ export declare function probeFailure(outcome: Exclude<ProbeOutcome, {
174
+ status: "gated";
175
+ }>, url: string): Fetched;
176
+ /**
177
+ * Assemble the `payment-signature` header. For an N-leg quote (a publisher with extra
178
+ * settlement legs, e.g. an operator fee), sign one payload per advertised leg and emit
179
+ * them as the ARRAY the gate's `verifyAndSettle` parses (leg order, author first). For
180
+ * a stock single-author quote, emit today's BARE single payload — byte-identical, so a
181
+ * non-fee toll is untouched. `signLeg` is the payment mode's per-leg signer (mock /
182
+ * memo / gateway); it receives the leg's substituted requirements + the leg's nonce and
183
+ * returns the raw payload object (this helper does the single-vs-array framing + base64).
184
+ */
185
+ export declare function assemblePayment(quoted: Quoted, signLeg: (req: LegRequirements, nonce?: string) => unknown | Promise<unknown>): Promise<string>;
186
+ /**
187
+ * Re-read an essay using a held Citation License instead of paying. Mode-agnostic
188
+ * — it's just an authenticated GET; the gate honors the license and serves free.
189
+ */
190
+ export declare function rereadWithLicense(url: string, kind: "read" | "citation", license: string, agentId: string,
191
+ /** Holder-of-key proof (`<ts>.<nonce>.<sig>`); required for a cnf-bound license. */
192
+ proof?: string): Promise<Fetched>;
193
+ export declare function selectBuyer(): Promise<Buyer>;
194
+ export { AGENT_UA };
195
+ //# sourceMappingURL=buyer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buyer.d.ts","sourceRoot":"","sources":["../src/buyer.ts"],"names":[],"mappings":"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"}
package/dist/buyer.js ADDED
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Buyer abstraction — how the Wayfarer prices and pays for an article.
3
+ *
4
+ * Pricing is the same in both modes: probe the tollgate, read the x402
5
+ * `PAYMENT-REQUIRED` header (free — no payment yet). Paying differs:
6
+ * - mock: sign a simple offline payment-signature the mock gate accepts.
7
+ * - gateway: Circle's GatewayClient does the full deposit-backed 402 flow.
8
+ */
9
+ import { activeNetwork, getConfig, supportsMemo } from "@naulon/shared";
10
+ import { agentFetch } from "./sign.js";
11
+ const AGENT_UA = "naulon-wayfarer/0.1";
12
+ /**
13
+ * The buyer's true outflow for a quote, in atomic micro-USDC (integer) — the sum of
14
+ * every advertised settlement leg, or the single author amount when there are none.
15
+ * Integer math only (AGENTS.md: money is integer micro-USDC, never floats).
16
+ */
17
+ export function quotedTotalAtomic(quoted) {
18
+ // Threshold MUST match assemblePayment (> 1): the signer only pays the `legs` array
19
+ // when there are 2+ legs, otherwise it signs `requirements.amount`. Summing a lone leg
20
+ // here would check a different number than gets signed — a gate could advertise a real
21
+ // price in requirements and one fake-cheap leg to slip past the ceiling. Check == sign.
22
+ if (quoted.legs && quoted.legs.length > 1) {
23
+ return quoted.legs.reduce((sum, leg) => sum + BigInt(leg.amount), 0n);
24
+ }
25
+ return BigInt(quoted.amountAtomic);
26
+ }
27
+ /**
28
+ * The toll-moved guard. If `guard` is set and the LIVE quote's true total tops the
29
+ * guard ceiling, returns a typed `toll_moved` failure (the buyer pays NOTHING);
30
+ * otherwise null (clear to pay). This is "re-quote at pay time, abort beyond
31
+ * tolerance": the buyer's own pre-pay probe is the re-quote, compared here against
32
+ * the ceiling the caller authorized.
33
+ */
34
+ export function tollMovedOrNull(quoted, guard) {
35
+ if (!guard)
36
+ return null;
37
+ const live = quotedTotalAtomic(quoted);
38
+ const ceiling = BigInt(guard.maxTotalAtomic);
39
+ if (live <= ceiling)
40
+ return null;
41
+ return {
42
+ ok: false,
43
+ errorCode: "toll_moved",
44
+ retryable: true,
45
+ error: `Toll moved at pay time: the live total is ${live} atomic but only ${ceiling} was authorized ` +
46
+ `(the quoted total plus tolerance). Re-quote and decide again — nothing was paid.`,
47
+ };
48
+ }
49
+ /**
50
+ * Classify a gate's payment-rejection message (BUY-1.4). The gate surfaces the real
51
+ * reason in its 402 body; this maps the known signals onto a typed code + a retry
52
+ * verdict. Insufficient funds is the one hard stop — every other rejection is worth a
53
+ * re-quote. Conservative by design: an unrecognized reason is `rejected` (retryable),
54
+ * never silently treated as a fundable hard stop.
55
+ */
56
+ /**
57
+ * Classify a THROWN session-signer refusal (BUY-4 hosted path). The cloud injects a
58
+ * grant-guarded session signer whose `signTypedData` throws an Error whose message is the
59
+ * sign guard's code, optionally suffixed " (remaining <micro>)". Unlike `classifyPaymentError`
60
+ * (which reads a gate 402 body), this refusal never reached the gate — nothing was paid — and
61
+ * must NOT be run through the 402 classifier, where `grant_exceeded`/`no_session` would fall
62
+ * through to a retryable `rejected`, telling the agent to retry a pay that can only fail again.
63
+ *
64
+ * grant_exceeded · leg_too_large · no_session → needs_topup (fund the session)
65
+ * grant_expired → grant_expired (renew — funds intact)
66
+ * bad_from · chain_mismatch · payee_not_allowed → rejected (a config error a top-up can't fix)
67
+ *
68
+ * Returns null when the message is NOT a known signer code (a real socket / unknown throw the
69
+ * caller should surface as `origin_error`). Never retryable: every signer refusal needs a
70
+ * host-side action (fund / renew / fix config), never a blind re-call.
71
+ */
72
+ export function classifySignerRefusal(errorText) {
73
+ const code = errorText.toLowerCase().match(/^([a-z_]+)/)?.[1];
74
+ switch (code) {
75
+ case "grant_exceeded":
76
+ case "leg_too_large":
77
+ case "no_session":
78
+ return { errorCode: "needs_topup", retryable: false };
79
+ case "grant_expired":
80
+ return { errorCode: "grant_expired", retryable: false };
81
+ case "bad_from":
82
+ case "chain_mismatch":
83
+ case "payee_not_allowed":
84
+ return { errorCode: "rejected", retryable: false };
85
+ default:
86
+ return null;
87
+ }
88
+ }
89
+ export function classifyPaymentError(errorText) {
90
+ const t = errorText.toLowerCase();
91
+ if (/insufficient|exceeds balance|transfer amount exceeds|not enough|balance too low/.test(t)) {
92
+ return { errorCode: "insufficient_funds", retryable: false };
93
+ }
94
+ if (/validity_too_short|validity too short|expired|valid ?before|too short|window/.test(t)) {
95
+ return { errorCode: "expired", retryable: true };
96
+ }
97
+ return { errorCode: "rejected", retryable: true };
98
+ }
99
+ /** Shared price probe — classify the gate's response by HTTP status, decoding the 402
100
+ * PAYMENT-REQUIRED header only for a real toll. Never throws: a broken 402 body or a
101
+ * network failure is returned as a typed outcome, not an exception. */
102
+ export async function probe(url, kind, agentId) {
103
+ let res;
104
+ try {
105
+ res = await agentFetch(url, {
106
+ headers: { "user-agent": AGENT_UA, "x-naulon-agent": agentId, "x-naulon-kind": kind },
107
+ });
108
+ }
109
+ catch {
110
+ // A DNS/connection failure is unreachable, not "free" — httpStatus 0 = no response.
111
+ return { status: "unreachable", httpStatus: 0 };
112
+ }
113
+ if (res.status === 402) {
114
+ const header = res.headers.get("payment-required");
115
+ if (!header)
116
+ return { status: "malformed", reason: "missing the PAYMENT-REQUIRED header" };
117
+ let decoded;
118
+ try {
119
+ decoded = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
120
+ }
121
+ catch {
122
+ return { status: "malformed", reason: "an undecodable PAYMENT-REQUIRED header" };
123
+ }
124
+ const req = decoded.accepts?.[0];
125
+ if (!req)
126
+ return { status: "malformed", reason: "a 402 with no payment options (empty accepts)" };
127
+ // The author leg is what the agent appraises (the content's price), so `priceUsdc`
128
+ // stays the author amount even when an additive fee leg makes the buyer's TOTAL
129
+ // higher. `legs` (when present) is the full set the buyer must sign — see assemblePayment.
130
+ const legs = decoded.extensions?.naulonLegs?.legs;
131
+ return {
132
+ status: "gated",
133
+ quoted: {
134
+ priceUsdc: Number(req.amount) / 1_000_000,
135
+ amountAtomic: req.amount,
136
+ nonce: req.extra?.nonce,
137
+ requirements: req,
138
+ ...(decoded.resource !== undefined ? { resource: decoded.resource } : {}),
139
+ // Only a real multi-leg (>1) quote carries `legs`; an honest gate emits naulonLegs
140
+ // solely for 2+ legs (build402). A lone-leg array is anomalous — dropping it keeps
141
+ // the invariant "legs present ⟺ signed as an array" that the ceiling check relies on.
142
+ ...(legs && legs.length > 1 ? { legs } : {}),
143
+ },
144
+ };
145
+ }
146
+ if (res.status === 404)
147
+ return { status: "not_found", httpStatus: 404 };
148
+ if (res.ok)
149
+ return { status: "free" };
150
+ return { status: "unreachable", httpStatus: res.status };
151
+ }
152
+ /** Back-compat thin wrapper: the decoded quote for a gated 402, else null. Callers that
153
+ * must distinguish free / not_found / unreachable use `probe()` directly. */
154
+ export async function probePrice(url, kind, agentId) {
155
+ const outcome = await probe(url, kind, agentId);
156
+ return outcome.status === "gated" ? outcome.quoted : null;
157
+ }
158
+ /**
159
+ * Map a NON-gated probe outcome to the typed `Fetched` failure every buyer returns, so a
160
+ * 404/5xx/malformed response never masquerades as a paid or free success. `not_gated` is
161
+ * reserved for the one true free (2xx) read; a 404 is `not_found` with a message that
162
+ * points the agent at the canonical url (the usual cause is the `/essays/<slug>` fallback
163
+ * not matching a `/articles/<slug>` publisher).
164
+ */
165
+ export function probeFailure(outcome, url) {
166
+ switch (outcome.status) {
167
+ case "free":
168
+ return {
169
+ ok: false,
170
+ errorCode: "not_gated",
171
+ retryable: false,
172
+ error: "not gated — the source returned a free (2xx) read; no payment is required.",
173
+ };
174
+ case "not_found":
175
+ return {
176
+ ok: false,
177
+ errorCode: "not_found",
178
+ retryable: false,
179
+ error: `probed ${url} — HTTP 404. This is NOT a free read: the path was not found. Pass the canonical ` +
180
+ `url from naulon_discover — the /essays/<slug> fallback does not match every publisher (many serve ` +
181
+ `/articles/<slug> or a custom path).`,
182
+ };
183
+ case "unreachable":
184
+ return {
185
+ ok: false,
186
+ errorCode: "origin_error",
187
+ retryable: true,
188
+ error: `probed ${url} — HTTP ${outcome.httpStatus || "no response"}. The origin/gate is unreachable or erroring; retry.`,
189
+ };
190
+ case "malformed":
191
+ return {
192
+ ok: false,
193
+ errorCode: "rejected",
194
+ retryable: true,
195
+ error: `the gate returned a 402 but ${outcome.reason}; cannot quote the toll. Retry or check the gate.`,
196
+ };
197
+ }
198
+ }
199
+ /**
200
+ * Assemble the `payment-signature` header. For an N-leg quote (a publisher with extra
201
+ * settlement legs, e.g. an operator fee), sign one payload per advertised leg and emit
202
+ * them as the ARRAY the gate's `verifyAndSettle` parses (leg order, author first). For
203
+ * a stock single-author quote, emit today's BARE single payload — byte-identical, so a
204
+ * non-fee toll is untouched. `signLeg` is the payment mode's per-leg signer (mock /
205
+ * memo / gateway); it receives the leg's substituted requirements + the leg's nonce and
206
+ * returns the raw payload object (this helper does the single-vs-array framing + base64).
207
+ */
208
+ export async function assemblePayment(quoted, signLeg) {
209
+ if (quoted.legs && quoted.legs.length > 1) {
210
+ const payloads = await Promise.all(quoted.legs.map((leg) => signLeg({ ...quoted.requirements, payTo: leg.payTo, amount: leg.amount }, leg.nonce)));
211
+ return Buffer.from(JSON.stringify(payloads)).toString("base64");
212
+ }
213
+ const payload = await signLeg(quoted.requirements, quoted.nonce);
214
+ return Buffer.from(JSON.stringify(payload)).toString("base64");
215
+ }
216
+ /**
217
+ * Re-read an essay using a held Citation License instead of paying. Mode-agnostic
218
+ * — it's just an authenticated GET; the gate honors the license and serves free.
219
+ */
220
+ export async function rereadWithLicense(url, kind, license, agentId,
221
+ /** Holder-of-key proof (`<ts>.<nonce>.<sig>`); required for a cnf-bound license. */
222
+ proof) {
223
+ const headers = {
224
+ "user-agent": AGENT_UA,
225
+ "x-naulon-agent": agentId,
226
+ "x-naulon-kind": kind,
227
+ "x-naulon-license": license,
228
+ };
229
+ if (proof)
230
+ headers["x-naulon-proof"] = proof;
231
+ const res = await agentFetch(url, { headers });
232
+ if (!res.ok)
233
+ return { ok: false, error: `re-read returned ${res.status}` };
234
+ return { ok: true, content: await res.text(), paidUsdc: 0, license };
235
+ }
236
+ export async function selectBuyer() {
237
+ const cfg = getConfig();
238
+ if (cfg.PAYMENT_MODE === "gateway") {
239
+ // On a memo-capable network (Arc) the gate settles via the self-relay rail, which
240
+ // expects a RAW USDC EIP-3009 authorization (USDC domain), not Circle's Gateway
241
+ // payload (GatewayWallet domain) — so the buyer signs differently. Field-presence
242
+ // gate, mirroring the gate's settle routing: a swap to Base falls back to the SDK.
243
+ if (supportsMemo(activeNetwork())) {
244
+ const { memoBuyer } = await import("./memo.js");
245
+ return memoBuyer();
246
+ }
247
+ const { gatewayBuyer } = await import("./gateway.js");
248
+ return gatewayBuyer();
249
+ }
250
+ const { mockBuyer } = await import("./pay.js");
251
+ return mockBuyer();
252
+ }
253
+ export { AGENT_UA };
254
+ //# sourceMappingURL=buyer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buyer.js","sourceRoot":"","sources":["../src/buyer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ import type { AppraisedCandidate, Decision } from "./types.ts";
2
+ export interface DecisionPolicy {
3
+ /**
4
+ * Don't pay for anything below this relevance, however cheap. Protects the
5
+ * budget from spending on near-misses just because they're affordable.
6
+ */
7
+ relevanceFloor: number;
8
+ /** Hard ceiling on how many essays to pay for in one run. */
9
+ maxPaid: number;
10
+ /**
11
+ * Agent identity this run is attributed to. It does NOT alter spend math here —
12
+ * a per-agent cap is applied by the caller resolving that agent's remaining
13
+ * allowance and passing it as `budgetUsdc`. It rides on the policy so the
14
+ * decision log can be tagged per agent in the audit plane (BUY-3.3).
15
+ */
16
+ agentId?: string;
17
+ /**
18
+ * Allowlist of publisher hosts. When set, ONLY these hosts are payable; every
19
+ * other host — and any candidate whose host is unknown — is skipped
20
+ * (deny-by-default). Host match is exact, case-insensitive.
21
+ */
22
+ allowDomains?: string[];
23
+ /** Hosts that are never paid, even when allowed and affordable. Deny wins over allow. */
24
+ denyDomains?: string[];
25
+ /**
26
+ * Max essays to pay for from any single host. Counts pays made this run plus
27
+ * any prior pays for the host in the current window (`context.priorDomainCounts`).
28
+ */
29
+ perDomainCap?: number;
30
+ /**
31
+ * A toll at or above this price is not auto-paid — it becomes an `approve`
32
+ * decision (human gate) instead. Cheaper tolls pay automatically.
33
+ */
34
+ approvalThresholdUsdc?: number;
35
+ /** Kill-switch: when true, halt all new spend this run (free re-reads still allowed). */
36
+ killSwitch?: boolean;
37
+ }
38
+ /** Runtime state injected into `decide()` that can't be known from the candidates alone. */
39
+ export interface DecideContext {
40
+ /**
41
+ * Pays already made to each host in the current rate-cap window (across earlier
42
+ * runs), added to this run's per-host count when enforcing `perDomainCap`.
43
+ */
44
+ priorDomainCounts?: Record<string, number>;
45
+ /**
46
+ * The configured gate base URL. Supplied so `decide()` can resolve a slug-only candidate to the
47
+ * SAME url the pay step will use (`c.url ?? articleUrl(gateBase, c.slug)`), and evaluate domain
48
+ * policy against that real target. Without it a slug-only candidate has no derivable host and is
49
+ * treated as unknown — which an allowlist correctly denies by default.
50
+ */
51
+ gateBase?: string;
52
+ }
53
+ /**
54
+ * The hostname money will actually go to, mirroring the pay step's own resolution
55
+ * (`c.url ?? articleUrl(gateBase, c.slug)`). This — not the discovery source's `Candidate.host`
56
+ * field — is what domain policy is evaluated against, so an allow/deny decision can never be made
57
+ * about a different host than the one that gets paid. Returns undefined when no url is derivable
58
+ * (an allowlist then denies by default).
59
+ */
60
+ export declare function payUrlOf(url: string | undefined, gateBase: string | undefined, slug: string): string | undefined;
61
+ export declare function payHostOf(url: string | undefined, gateBase: string | undefined, slug: string): string | undefined;
62
+ /** The shared spend gate's verdict. `approve` means "real, but needs a human" — distinct from
63
+ * `skip` so callers can surface a human-approval affordance rather than a flat refusal. */
64
+ export type SpendVerdict = {
65
+ ok: true;
66
+ } | {
67
+ ok: false;
68
+ action: "skip" | "approve";
69
+ reason: string;
70
+ };
71
+ /**
72
+ * THE operator-policy gate — the single source of truth for "may I pay this host, at this price,
73
+ * right now". Every spending path calls this: `decide()` for the composite research run, and the
74
+ * MCP's granular `naulon_pay_and_read`.
75
+ *
76
+ * It exists because the checks previously lived ONLY inside `decide()`, so the granular pay tool
77
+ * — the path the tool descriptions tell agents to prefer — silently ignored the kill-switch,
78
+ * deny/allow lists, per-domain cap, and approval threshold an operator had configured. Two
79
+ * implementations of one rule is how that bug happens; there is now one implementation.
80
+ *
81
+ * Order is load-bearing and matches the original `decide()` sequence, so the reason a caller
82
+ * surfaces when several gates apply is unchanged: kill → deny → allow → maxPaid → perDomainCap →
83
+ * budget → approval. `paidCount` / `remainingUsdc` are optional; omit them when the caller
84
+ * enforces those with its own accounting and messaging (the MCP session envelope does).
85
+ */
86
+ export declare function spendGate(input: {
87
+ /** Publisher host, already normalized-ish; undefined when unknown (deny-by-default under an allowlist). */
88
+ host: string | undefined;
89
+ /** The buyer's TRUE total for this read, in USDC. */
90
+ priceUsdc: number;
91
+ policy: DecisionPolicy;
92
+ /** Pays already made for this host (this run/session + any prior window). */
93
+ paidForHost?: number;
94
+ /** Pays already made overall — enables the `maxPaid` gate when provided. */
95
+ paidCount?: number;
96
+ /** Budget left in USDC — enables the budget gate when provided. */
97
+ remainingUsdc?: number;
98
+ }): SpendVerdict;
99
+ /**
100
+ * TODO(you): this policy is the lever that defines the agent's "taste". The
101
+ * defaults are sensible, but the interesting choices are yours to make:
102
+ *
103
+ * - relevanceFloor: how picky? Too low → wastes budget on tangential essays.
104
+ * Too high → misses useful context. 0.35 is a starting guess.
105
+ * - Ranking key: density (relevance/price) favors cheap-and-relevant. Would
106
+ * you instead rank by raw relevance (quality at any price), or blend them?
107
+ * See `rank()` below — that sort is the whole strategy in one line.
108
+ * - maxPaid: a stop so a big budget doesn't over-cite a thin topic.
109
+ *
110
+ * Tune these against real runs and watch the decision log; that visible
111
+ * reasoning is what the judges reward.
112
+ */
113
+ export declare const DEFAULT_POLICY: DecisionPolicy;
114
+ export declare function decide(candidates: AppraisedCandidate[], budgetUsdc: number, cached?: ReadonlySet<string>, policy?: DecisionPolicy, context?: DecideContext): Decision[];
115
+ //# sourceMappingURL=decide.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../src/decide.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE/D,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAIhB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,yFAAyF;IACzF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,yFAAyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAOD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAYhH;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjH;AAED;4FAC4F;AAC5F,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpG;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE;IAC/B,2GAA2G;IAC3G,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,cAAc,CAAC;IACvB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,YAAY,CAwCf;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,EAAE,cAG5B,CAAC;AASF,wBAAgB,MAAM,CACpB,UAAU,EAAE,kBAAkB,EAAE,EAChC,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,WAAW,CAAC,MAAM,CAAa,EACvC,MAAM,GAAE,cAA+B,EACvC,OAAO,GAAE,aAAkB,GAC1B,QAAQ,EAAE,CA2FZ"}