@paidfor/mcp 0.5.0 → 0.7.0
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 +49 -7
- package/dist/clients.d.ts +114 -0
- package/dist/clients.js +330 -0
- package/dist/init.d.ts +61 -0
- package/dist/init.js +629 -0
- package/dist/link.d.ts +45 -0
- package/dist/link.js +109 -0
- package/dist/pay.d.ts +16 -1
- package/dist/pay.js +58 -7
- package/dist/pending.d.ts +25 -0
- package/dist/pending.js +66 -0
- package/dist/rules.js +1 -1
- package/dist/server.d.ts +3 -8
- package/dist/server.js +4 -13
- package/dist/setup.d.ts +3 -1
- package/dist/setup.js +5 -5
- package/dist/stdio.js +18 -2
- package/dist/team.d.ts +80 -0
- package/dist/team.js +170 -0
- package/dist/wallet.d.ts +10 -0
- package/dist/wallet.js +22 -7
- package/package.json +2 -2
package/dist/link.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linking a wallet to a paidfor account from the command line, as a device flow: `POST /v1/team/link` returns a short
|
|
3
|
+
* code a person checks and approves in the browser, and `POST /v1/team/link/poll` returns the team key once, after the
|
|
4
|
+
* approval. The device code and the team key are secrets: they are returned to the caller and never put in a message.
|
|
5
|
+
*/
|
|
6
|
+
import { assertKeySafeBase } from "./wallet.js";
|
|
7
|
+
/** A non-empty string. */
|
|
8
|
+
const str = (v) => typeof v === "string" && v.length > 0;
|
|
9
|
+
/** POSTs JSON and reads the JSON answer; follows no redirect, gives up after 15 seconds. */
|
|
10
|
+
async function postJson(fetchFn, url, body, client) {
|
|
11
|
+
const res = await fetchFn(url, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", "X-Paidfor-Client": client },
|
|
14
|
+
body: JSON.stringify(body),
|
|
15
|
+
redirect: "error",
|
|
16
|
+
signal: AbortSignal.timeout(15_000),
|
|
17
|
+
});
|
|
18
|
+
const json = (await res.json().catch(() => ({})));
|
|
19
|
+
return { status: res.status, json: json && typeof json === "object" ? json : {} };
|
|
20
|
+
}
|
|
21
|
+
/** Starts a link. Throws an Error whose message is safe to show (never a code or a key) when it cannot. */
|
|
22
|
+
export async function startLink(o) {
|
|
23
|
+
const base = o.baseUrl.replace(/\/$/, "");
|
|
24
|
+
assertKeySafeBase(base);
|
|
25
|
+
let r;
|
|
26
|
+
try {
|
|
27
|
+
r = await postJson(o.fetchFn ?? fetch, `${base}/v1/team/link`, { address: o.address, label: o.label, client: o.client }, o.client);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
throw new Error(`could not reach ${base}: ${e instanceof Error ? e.message : String(e)}`);
|
|
31
|
+
}
|
|
32
|
+
if (r.status === 429)
|
|
33
|
+
throw new Error("paidfor is taking too many link requests right now (HTTP 429)");
|
|
34
|
+
if (r.status === 503)
|
|
35
|
+
throw new Error("paidfor cannot link accounts right now (HTTP 503)");
|
|
36
|
+
if (r.status !== 201 && r.status !== 200)
|
|
37
|
+
throw new Error(`paidfor answered HTTP ${r.status}${str(r.json.error) ? `: ${r.json.error.slice(0, 200)}` : ""}`);
|
|
38
|
+
const j = r.json;
|
|
39
|
+
const interval = typeof j.interval === "number" && Number.isFinite(j.interval) && j.interval > 0 ? j.interval : 5;
|
|
40
|
+
if (!str(j.user_code) || !str(j.device_code) || !str(j.verify_url) || !str(j.expires_at) || Number.isNaN(Date.parse(j.expires_at))) {
|
|
41
|
+
throw new Error("paidfor's answer to the link request could not be read");
|
|
42
|
+
}
|
|
43
|
+
return { user_code: j.user_code, device_code: j.device_code, verify_url: j.verify_url, expires_at: j.expires_at, interval };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Polls until the link is approved, denied, expired or already used, or `expires_at` passes. A network error or a 5xx
|
|
47
|
+
* is tried again on the next tick; a 429 slows the polling down. `immediate` asks once before the first wait (a link
|
|
48
|
+
* resumed from an earlier run); `stopAt` (epoch ms) gives up with { kind: "pending" } when the next poll would be later.
|
|
49
|
+
*/
|
|
50
|
+
export async function pollLink(o) {
|
|
51
|
+
const base = o.baseUrl.replace(/\/$/, "");
|
|
52
|
+
const fetchFn = o.fetchFn ?? fetch;
|
|
53
|
+
const sleep = o.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
54
|
+
const now = o.now ?? Date.now;
|
|
55
|
+
const deadline = Date.parse(o.start.expires_at);
|
|
56
|
+
let interval = o.start.interval;
|
|
57
|
+
let first = true;
|
|
58
|
+
const stillPending = { kind: "pending", message: "not approved yet" };
|
|
59
|
+
// Whether to wait for another tick, or stop here because the window closes first.
|
|
60
|
+
/** Whether another poll fits before stopAt. */
|
|
61
|
+
const goOn = () => o.stopAt === undefined || now() + interval * 1000 <= o.stopAt;
|
|
62
|
+
for (;;) {
|
|
63
|
+
if (!(first && o.immediate))
|
|
64
|
+
await sleep(interval * 1000);
|
|
65
|
+
first = false;
|
|
66
|
+
if (now() >= deadline)
|
|
67
|
+
return { kind: "expired", message: "the code expired before it was approved" };
|
|
68
|
+
let r;
|
|
69
|
+
try {
|
|
70
|
+
r = await postJson(fetchFn, `${base}/v1/team/link/poll`, { device_code: o.start.device_code }, o.client);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
if (!goOn())
|
|
74
|
+
return stillPending;
|
|
75
|
+
continue; // try again on the next tick, until the deadline
|
|
76
|
+
}
|
|
77
|
+
if (r.status === 410)
|
|
78
|
+
return { kind: "used", message: "this link was already used" };
|
|
79
|
+
if (r.status === 429) {
|
|
80
|
+
interval += 5;
|
|
81
|
+
if (!goOn())
|
|
82
|
+
return stillPending;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (r.status >= 500) {
|
|
86
|
+
if (!goOn())
|
|
87
|
+
return stillPending;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (r.status !== 200)
|
|
91
|
+
return { kind: "failed", message: `paidfor answered HTTP ${r.status}${str(r.json.error) ? `: ${r.json.error.slice(0, 200)}` : ""}` };
|
|
92
|
+
switch (r.json.status) {
|
|
93
|
+
case "pending":
|
|
94
|
+
if (!goOn())
|
|
95
|
+
return stillPending;
|
|
96
|
+
continue;
|
|
97
|
+
case "approved":
|
|
98
|
+
if (!str(r.json.team_key))
|
|
99
|
+
return { kind: "failed", message: "paidfor approved the link but sent no key" };
|
|
100
|
+
return { kind: "approved", team_key: r.json.team_key, team_name: str(r.json.team_name) ? r.json.team_name : "your account" };
|
|
101
|
+
case "denied":
|
|
102
|
+
return { kind: "denied", message: "the link was denied in the browser" };
|
|
103
|
+
case "expired":
|
|
104
|
+
return { kind: "expired", message: "the code expired before it was approved" };
|
|
105
|
+
default:
|
|
106
|
+
return { kind: "failed", message: "paidfor's answer could not be read" };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
package/dist/pay.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { type Receipt, type SafeNetwork } from "@paidfor/x402-safe-fetch";
|
|
3
|
+
import { type TeamDecision } from "./team.js";
|
|
3
4
|
import { type Limits, type LoadedWallet } from "./wallet.js";
|
|
4
5
|
export declare const PAY_TOOL_NAMES: readonly ["pay_x402", "fetch_paid_url", "wallet_status"];
|
|
5
6
|
/** The second name for the pay_x402 handler, registered only with PayOptions.fetchAlias (PAIDFOR_FETCH_ALIAS=1 on stdio). */
|
|
@@ -8,7 +9,7 @@ export declare const BODY_LIMIT_BYTES: number;
|
|
|
8
9
|
/** When to reuse an intent_id: a retry of the same purchase only. A date-only id (observed: "bitcoin-price-2026-11-01" chosen in three fresh sessions) replays instead of buying. */
|
|
9
10
|
export declare const INTENT_RULE: string;
|
|
10
11
|
export declare const PAY_INSTRUCTIONS: string;
|
|
11
|
-
/** The stdio server's instructions with a wallet configured: the pay paragraph goes
|
|
12
|
+
/** The stdio server's instructions with a wallet configured: the pay paragraph goes last. */
|
|
12
13
|
export declare const INSTRUCTIONS_WITH_PAY: string;
|
|
13
14
|
export declare const PAY_TITLE = "Fetch a paid (x402) URL, paying with your wallet";
|
|
14
15
|
export declare const PAY_ALIAS_TITLE = "Fetch a paid URL (same as pay_x402)";
|
|
@@ -30,6 +31,10 @@ export type PayOptions = {
|
|
|
30
31
|
balanceOf?: (address: `0x${string}`) => Promise<bigint>;
|
|
31
32
|
/** Also register the same handler as fetch_paid_url (PAY_ALIAS). Off by default; stdio turns it on with PAIDFOR_FETCH_ALIAS=1. */
|
|
32
33
|
fetchAlias?: boolean;
|
|
34
|
+
/** PAIDFOR_TEAM_KEY: ask the team's rules before every payment and report the outcome. Stdio only. */
|
|
35
|
+
teamKey?: string;
|
|
36
|
+
/** Tests only: how long to wait before the second try of an outcome report. */
|
|
37
|
+
teamRetryDelayMs?: number;
|
|
33
38
|
};
|
|
34
39
|
/** The receipt as pay_x402 reports it: what was paid, to whom, the proof, and how to check it. */
|
|
35
40
|
export declare function receiptSummary(r: Receipt): {
|
|
@@ -57,6 +62,16 @@ export declare function receiptSummary(r: Receipt): {
|
|
|
57
62
|
export declare function describeAge(ms: number): string;
|
|
58
63
|
/** The first line of a replayed pay_x402 result: the age and time of the stored result, so a second genuine purchase is not mistaken for a retry. */
|
|
59
64
|
export declare function replayHead(r: Receipt, now?: number): string;
|
|
65
|
+
/** What pay_x402 says when the team's rules, not the seller or the local caps, stop a payment. Nothing was paid in any of these. */
|
|
66
|
+
export declare function teamRefusal(d: Exclude<TeamDecision, {
|
|
67
|
+
kind: "approve";
|
|
68
|
+
}>, intentId: string, limits: unknown): {
|
|
69
|
+
isError?: boolean | undefined;
|
|
70
|
+
content: {
|
|
71
|
+
type: "text";
|
|
72
|
+
text: string;
|
|
73
|
+
}[];
|
|
74
|
+
};
|
|
60
75
|
export declare function truncateBody(body: string): {
|
|
61
76
|
body: string;
|
|
62
77
|
note: string | null;
|
package/dist/pay.js
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* user's own wallet and report the verified outcome to the index. Only stdio.ts imports this module;
|
|
4
4
|
* the hosted service (index.ts) and the site's /mcp route never do, because they never hold a key.
|
|
5
5
|
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { mkdir } from "node:fs/promises";
|
|
7
8
|
import { join } from "node:path";
|
|
8
9
|
import { PaymentRefusedError, SafeFetchError, safeFetch } from "@paidfor/x402-safe-fetch";
|
|
9
10
|
import { createPublicClient, erc20Abi, http } from "viem";
|
|
10
11
|
import { base as baseChain } from "viem/chains";
|
|
11
12
|
import { z } from "zod";
|
|
12
|
-
import { INSTRUCTIONS,
|
|
13
|
+
import { INSTRUCTIONS, PAID_URL_SIGNALS, PAY_ROUTING_RULE, PLAIN_FETCH_FAILS } from "./server.js";
|
|
14
|
+
import { TeamClient, outcomeFor } from "./team.js";
|
|
13
15
|
import { SpendLedger, fromMicro, paidforDir, toMicro } from "./wallet.js";
|
|
14
16
|
export const PAY_TOOL_NAMES = ["pay_x402", "fetch_paid_url", "wallet_status"];
|
|
15
17
|
/** The second name for the pay_x402 handler, registered only with PayOptions.fetchAlias (PAIDFOR_FETCH_ALIAS=1 on stdio). */
|
|
@@ -27,8 +29,8 @@ export const PAY_INSTRUCTIONS = `${PAY_ROUTING_RULE} ` +
|
|
|
27
29
|
`${INTENT_RULE} ` +
|
|
28
30
|
"Tell the user the amount and the seller verdict before the first purchase from a new seller. " +
|
|
29
31
|
"pay_x402 signs with the user's own wallet on this machine; the key never leaves it.";
|
|
30
|
-
/** The stdio server's instructions with a wallet configured: the pay paragraph goes
|
|
31
|
-
export const INSTRUCTIONS_WITH_PAY = INSTRUCTIONS
|
|
32
|
+
/** The stdio server's instructions with a wallet configured: the pay paragraph goes last. */
|
|
33
|
+
export const INSTRUCTIONS_WITH_PAY = `${INSTRUCTIONS}\n${PAY_INSTRUCTIONS}`;
|
|
32
34
|
export const PAY_TITLE = "Fetch a paid (x402) URL, paying with your wallet";
|
|
33
35
|
export const PAY_ALIAS_TITLE = "Fetch a paid URL (same as pay_x402)";
|
|
34
36
|
export const PAY_DESCRIPTION = "Fetches a paid (x402) URL end to end: sends the request, handles the HTTP 402 and pays the quoted USDC on Base with the user's own wallet under the configured caps, and returns the response body. " +
|
|
@@ -83,6 +85,20 @@ export function replayHead(r, now = Date.now()) {
|
|
|
83
85
|
const when = at && Number.isFinite(Date.parse(at)) ? `stored ${describeAge(now - Date.parse(at))} ago (${at})` : "stored earlier (time unknown, the original receipt file is gone)";
|
|
84
86
|
return `Replayed the result ${when} for intent ${r.intent_id}; nothing was paid again. If this is a new purchase, call again with a new intent_id.`;
|
|
85
87
|
}
|
|
88
|
+
/** What pay_x402 says when the team's rules, not the seller or the local caps, stop a payment. Nothing was paid in any of these. */
|
|
89
|
+
export function teamRefusal(d, intentId, limits) {
|
|
90
|
+
const res = (head, detail, isError = false) => text(`${head}\n\n${json({ ...detail, paid: false, intent_id: intentId, limits })}`, isError);
|
|
91
|
+
switch (d.kind) {
|
|
92
|
+
case "decline":
|
|
93
|
+
return res(`Declined by your team's rules: ${d.message}\nNothing was paid.`, { refused: "team_decline", reason: d.reason, ...(d.retry_after !== undefined ? { retry_after: d.retry_after } : {}) });
|
|
94
|
+
case "needs_approval":
|
|
95
|
+
return res(`A person has to approve this payment before it can be made. Nothing was paid.\nGive the user this link: ${d.approve_url}\nAfter they approve it, call pay_x402 again with intent_id "${intentId}". The request waits until ${d.expires_at}.${d.message ? ` ${d.message}` : ""}`, { refused: "team_needs_approval", approve_url: d.approve_url, expires_at: d.expires_at });
|
|
96
|
+
case "already_settled":
|
|
97
|
+
return res(`This purchase was already paid (settlement ${d.settlement_ref}, outcome ${d.outcome}); nothing was paid again. If this is a new purchase, call again with a new intent_id.`, { refused: "team_already_settled", settlement_ref: d.settlement_ref, outcome: d.outcome });
|
|
98
|
+
case "failed":
|
|
99
|
+
return res(`The team's rules could not be checked, so nothing was paid: ${d.message}\nCalling again with intent_id "${intentId}" is safe.`, { refused: "team_authorize_failed" }, true);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
86
102
|
export function truncateBody(body) {
|
|
87
103
|
const buf = Buffer.from(body, "utf8");
|
|
88
104
|
if (buf.byteLength <= BODY_LIMIT_BYTES)
|
|
@@ -103,6 +119,9 @@ export async function registerPayTools(server, o) {
|
|
|
103
119
|
const base = o.baseUrl.replace(/\/$/, "");
|
|
104
120
|
const address = o.wallet.account.address;
|
|
105
121
|
const logger = { warn: (m) => void process.stderr.write(`${m}\n`) };
|
|
122
|
+
const team = o.teamKey ? new TeamClient({ baseUrl: base, teamKey: o.teamKey, dir, ...(o.fetch ? { fetch: o.fetch } : {}), warn: logger.warn, ...(o.teamRetryDelayMs !== undefined ? { retryDelayMs: o.teamRetryDelayMs } : {}) }) : null;
|
|
123
|
+
if (team)
|
|
124
|
+
void team.flush();
|
|
106
125
|
const payInput = {
|
|
107
126
|
url: z.string().url().describe("The URL to fetch and, if it answers 402, pay for"),
|
|
108
127
|
method: z.string().min(1).max(10).optional().describe("HTTP method, default GET"),
|
|
@@ -124,6 +143,20 @@ export async function registerPayTools(server, o) {
|
|
|
124
143
|
spent_today_usdc: fromMicro(spent),
|
|
125
144
|
...(max_usd !== undefined && max_usd > o.limits.maxPerCallUsd ? { note: `max_usd ${max_usd} is above PAIDFOR_MAX_PER_CALL_USD; capped at ${o.limits.maxPerCallUsd}` } : {}),
|
|
126
145
|
};
|
|
146
|
+
// Team mode needs the id before the first call: a person's approval is claimed by asking again with the same one.
|
|
147
|
+
const intentId = intent_id ?? (team ? randomUUID() : undefined);
|
|
148
|
+
const asked = {};
|
|
149
|
+
/** Say what happened to an approved payment. Undefined when the server approved nothing. */
|
|
150
|
+
const settle = async (r) => {
|
|
151
|
+
const d = asked.decision;
|
|
152
|
+
if (!team || d?.kind !== "approve")
|
|
153
|
+
return undefined;
|
|
154
|
+
if (!r) {
|
|
155
|
+
logger.warn(`paidfor-mcp: no receipt for approved intent ${intentId}, so no outcome was reported; the approval keeps counting against the budgets`);
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
return team.report(outcomeFor(r, address, d.auth_ref));
|
|
159
|
+
};
|
|
127
160
|
const sf = safeFetch(o.wallet.account, {
|
|
128
161
|
maxPerCallUsd: cap,
|
|
129
162
|
maxOverListedRatio: 1.1,
|
|
@@ -134,31 +167,48 @@ export async function registerPayTools(server, o) {
|
|
|
134
167
|
logger,
|
|
135
168
|
...(o.network ? { network: o.network } : {}),
|
|
136
169
|
...(o.fetch ? { fetch: o.fetch } : {}),
|
|
170
|
+
...(team
|
|
171
|
+
? {
|
|
172
|
+
authorize: async (q) => {
|
|
173
|
+
const d = await team.authorize({ wallet: q.payer, intent_id: q.intentId, resource_url: q.url, pay_to: q.payTo, amount_atomic: q.amountAtomic, network: q.network, asset: q.asset, scheme: q.scheme, protocol: "x402" });
|
|
174
|
+
asked.decision = d;
|
|
175
|
+
return d.kind === "approve" ? { approve: true, nonce: d.auth_ref, validBefore: d.valid_before } : { approve: false, message: d.message };
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
: {}),
|
|
137
179
|
});
|
|
138
180
|
try {
|
|
139
|
-
|
|
181
|
+
// Owed reports go first: until the server has them it answers the same purchase with pending_settlement.
|
|
182
|
+
await team?.flush();
|
|
183
|
+
const res = await sf(url, { method: method ?? "GET", headers, body }, { intentId });
|
|
140
184
|
const r = res.receipt;
|
|
141
185
|
if (r.paid && !r.replayed)
|
|
142
186
|
await ledger.add(toMicro(r.amount_usdc));
|
|
187
|
+
const teamReport = await settle(r);
|
|
143
188
|
const { body: out, note } = truncateBody(await res.text());
|
|
144
189
|
const head = r.replayed
|
|
145
190
|
? replayHead(r)
|
|
146
191
|
: r.paid
|
|
147
192
|
? `Paid ${r.amount_usdc} USDC to ${r.quote?.pay_to}; the seller answered HTTP ${res.status} (${r.outcome}).`
|
|
148
193
|
: `No payment was needed; the server answered HTTP ${res.status}.`;
|
|
149
|
-
return text(`${head}\n\n${json({ status: res.status, receipt: receiptSummary(r), limits, ...(note ? { body_note: note } : {}) })}\n\n--- response body ---\n${out}`);
|
|
194
|
+
return text(`${head}\n\n${json({ status: res.status, receipt: receiptSummary(r), limits, ...(teamReport ? { team_outcome_report: teamReport } : {}), ...(note ? { body_note: note } : {}) })}\n\n--- response body ---\n${out}`);
|
|
150
195
|
}
|
|
151
196
|
catch (e) {
|
|
197
|
+
if (asked.decision && asked.decision.kind !== "approve" && e instanceof PaymentRefusedError && e.code === "refused_authorize") {
|
|
198
|
+
return teamRefusal(asked.decision, e.receipt?.intent_id ?? intentId, limits);
|
|
199
|
+
}
|
|
200
|
+
const teamReport = await settle(e instanceof SafeFetchError ? e.receipt : undefined);
|
|
201
|
+
const reported = teamReport ? { team_outcome_report: teamReport } : {};
|
|
152
202
|
if (e instanceof PaymentRefusedError) {
|
|
153
203
|
const why = e.code === "refused_max_per_call" && dailyBinds
|
|
154
204
|
? `refused: today's cap of ${o.limits.dailyCapUsd} USD (PAIDFOR_DAILY_CAP_USD) leaves ${fromMicro(Math.max(leftMicro, 0))} USDC. ${e.message}`
|
|
155
205
|
: e.code === "refused_max_per_call"
|
|
156
206
|
? `refused: the quote is above this call's cap of ${cap} USD (max_usd, capped by PAIDFOR_MAX_PER_CALL_USD ${o.limits.maxPerCallUsd}). ${e.message}`
|
|
157
207
|
: `refused: ${e.message}`;
|
|
158
|
-
return text(`${why}\nNothing was paid.\n\n${json({ refused: e.code, paid: false, seller_url: e.sellerUrl ?? e.receipt?.seller.seller_url ?? null, intent_id: e.receipt?.intent_id ??
|
|
208
|
+
return text(`${why}\nNothing was paid.\n\n${json({ refused: e.code, paid: false, seller_url: e.sellerUrl ?? e.receipt?.seller.seller_url ?? null, intent_id: e.receipt?.intent_id ?? intentId ?? null, limits, ...reported })}`);
|
|
159
209
|
}
|
|
160
210
|
if (e instanceof SafeFetchError) {
|
|
161
|
-
return text(`${e.code}: ${e.message}\n\n${json({ error: e.code, paid: e.receipt?.paid ?? false, receipt: e.receipt ? receiptSummary(e.receipt) : null, limits })}`, true);
|
|
211
|
+
return text(`${e.code}: ${e.message}\n\n${json({ error: e.code, paid: e.receipt?.paid ?? false, receipt: e.receipt ? receiptSummary(e.receipt) : null, limits, ...reported })}`, true);
|
|
162
212
|
}
|
|
163
213
|
return text(`error: ${e instanceof Error ? e.message : String(e)}`, true);
|
|
164
214
|
}
|
|
@@ -193,6 +243,7 @@ export async function registerPayTools(server, o) {
|
|
|
193
243
|
max_per_call_usd: o.limits.maxPerCallUsd,
|
|
194
244
|
daily_cap_usd: o.limits.dailyCapUsd,
|
|
195
245
|
spent_today_usdc: fromMicro(spent),
|
|
246
|
+
...(team ? { team_mode: true } : {}),
|
|
196
247
|
};
|
|
197
248
|
return text(`${usdc === null ? "Balance unavailable" : `${usdc} USDC on Base`} at ${address}.\n\n${json(s)}`);
|
|
198
249
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The saved link: what `link` needs to poll it, register the wallet and write the configs. */
|
|
2
|
+
export type PendingLink = {
|
|
3
|
+
v: 1;
|
|
4
|
+
device_code: string;
|
|
5
|
+
user_code: string;
|
|
6
|
+
verify_url: string;
|
|
7
|
+
expires_at: string;
|
|
8
|
+
interval: number;
|
|
9
|
+
base_url: string;
|
|
10
|
+
label: string;
|
|
11
|
+
key_file: string;
|
|
12
|
+
address: string;
|
|
13
|
+
clients: string[];
|
|
14
|
+
/** Set once the link was approved but the registration failed: the key handed over, kept for the retry. */
|
|
15
|
+
team_key?: string;
|
|
16
|
+
team_name?: string;
|
|
17
|
+
};
|
|
18
|
+
/** Where the record lives: ~/.paidfor/link-pending.json. */
|
|
19
|
+
export declare const pendingPath: (home: string) => string;
|
|
20
|
+
/** The saved record, or null when there is none or it is unreadable or malformed. Never throws. */
|
|
21
|
+
export declare function readPending(home: string): Promise<PendingLink | null>;
|
|
22
|
+
/** Saves the record atomically (dir 700, file 600), replacing any earlier one. Returns the file's path. */
|
|
23
|
+
export declare function writePending(home: string, rec: Omit<PendingLink, "v">): Promise<string>;
|
|
24
|
+
/** Removes the record; a record already gone is fine. */
|
|
25
|
+
export declare function deletePending(home: string): Promise<void>;
|
package/dist/pending.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A link started by `init --yes --link` (or `link --yes`) that nobody waited for: an agent's shell returns output only
|
|
3
|
+
* when the command exits, so the command prints the approval link, saves this record and exits, and a later
|
|
4
|
+
* `paidfor-mcp link` resumes it. ~/.paidfor/link-pending.json (dir 700, file 600, atomic write) is the only place the
|
|
5
|
+
* device code is ever written. A record that cannot be read is treated as absent and replaced.
|
|
6
|
+
*
|
|
7
|
+
* A record that carries `team_key` means "approved, registration owed": the browser approval went through and the key
|
|
8
|
+
* was handed over (once), but registering the wallet failed. `link` retries the registration with that key instead of
|
|
9
|
+
* asking for a new approval. Such a record does not expire with the link: the key does not.
|
|
10
|
+
*/
|
|
11
|
+
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { paidforDir } from "./wallet.js";
|
|
14
|
+
/** Where the record lives: ~/.paidfor/link-pending.json. */
|
|
15
|
+
export const pendingPath = (home) => join(paidforDir(home), "link-pending.json");
|
|
16
|
+
/** A non-empty string. */
|
|
17
|
+
const str = (v) => typeof v === "string" && v.length > 0;
|
|
18
|
+
/** The saved record, or null when there is none or it is unreadable or malformed. Never throws. */
|
|
19
|
+
export async function readPending(home) {
|
|
20
|
+
try {
|
|
21
|
+
const j = JSON.parse(await readFile(pendingPath(home), "utf8"));
|
|
22
|
+
if (!j || typeof j !== "object")
|
|
23
|
+
return null;
|
|
24
|
+
const ok = str(j.device_code) &&
|
|
25
|
+
str(j.user_code) &&
|
|
26
|
+
str(j.verify_url) &&
|
|
27
|
+
str(j.expires_at) &&
|
|
28
|
+
!Number.isNaN(Date.parse(j.expires_at)) &&
|
|
29
|
+
typeof j.interval === "number" &&
|
|
30
|
+
j.interval > 0 &&
|
|
31
|
+
str(j.base_url) &&
|
|
32
|
+
typeof j.label === "string" &&
|
|
33
|
+
str(j.key_file) &&
|
|
34
|
+
str(j.address) &&
|
|
35
|
+
Array.isArray(j.clients) &&
|
|
36
|
+
j.clients.every((c) => typeof c === "string") &&
|
|
37
|
+
(j.team_key === undefined || str(j.team_key)) &&
|
|
38
|
+
(j.team_name === undefined || typeof j.team_name === "string");
|
|
39
|
+
return ok ? j : null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Saves the record atomically (dir 700, file 600), replacing any earlier one. Returns the file's path. */
|
|
46
|
+
export async function writePending(home, rec) {
|
|
47
|
+
const dir = paidforDir(home);
|
|
48
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
49
|
+
await chmod(dir, 0o700).catch(() => undefined);
|
|
50
|
+
const file = pendingPath(home);
|
|
51
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
52
|
+
try {
|
|
53
|
+
await writeFile(tmp, JSON.stringify({ v: 1, ...rec }) + "\n", { mode: 0o600 });
|
|
54
|
+
await chmod(tmp, 0o600);
|
|
55
|
+
await rename(tmp, file);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
await unlink(tmp).catch(() => undefined);
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
return file;
|
|
62
|
+
}
|
|
63
|
+
/** Removes the record; a record already gone is fine. */
|
|
64
|
+
export async function deletePending(home) {
|
|
65
|
+
await unlink(pendingPath(home)).catch(() => undefined);
|
|
66
|
+
}
|
package/dist/rules.js
CHANGED
|
@@ -34,7 +34,7 @@ export const RULES_SHA = "7258b901f6577a02dfe997416d3de61e122f5da21476eb686dd1c1
|
|
|
34
34
|
*/
|
|
35
35
|
export const REPORT_TRIGGER = "after your agent pays an x402 seller with any wallet";
|
|
36
36
|
export const REPORT_DESCRIPTION = "After your agent pays an x402 seller with any wallet, report what came back. Needs the resource URL, the settlement transaction hash and the outcome. " +
|
|
37
|
-
"Free
|
|
37
|
+
"Free; the report counts only with on-chain proof.";
|
|
38
38
|
/** The outcomes a reporter can name, in the index's words. */
|
|
39
39
|
export const REPORT_OUTCOMES = {
|
|
40
40
|
ok: "the response was usable for its purpose",
|
package/dist/server.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { REPORT_OUTCOMES } from "./rules.js";
|
|
3
3
|
export * from "./rules.js";
|
|
4
|
-
export declare const VERSION = "0.
|
|
4
|
+
export declare const VERSION = "0.7.0";
|
|
5
5
|
/** Sent on every request to the index so usage can be counted by channel (never anything else). */
|
|
6
|
-
export declare const CLIENT_HEADER = "mcp/0.
|
|
6
|
+
export declare const CLIENT_HEADER = "mcp/0.7.0";
|
|
7
7
|
/** What the hosted service (index.ts) sends instead, so the index can count hosted and stdio use apart. */
|
|
8
|
-
export declare const HOSTED_CLIENT_HEADER = "mcp-hosted/0.
|
|
8
|
+
export declare const HOSTED_CLIENT_HEADER = "mcp-hosted/0.7.0";
|
|
9
9
|
/**
|
|
10
10
|
* Which client product a hosted session comes from, read from `clientInfo.name` on initialize and nothing else from the
|
|
11
11
|
* handshake: a product name, never a user. The hosted service appends it to its header (`mcp-hosted/<version>+claude`),
|
|
@@ -17,11 +17,6 @@ export declare function clientTag(name: unknown): ClientTag;
|
|
|
17
17
|
export declare const taggedClientHeader: (tag: ClientTag | null) => string;
|
|
18
18
|
export declare const DEFAULT_BASE = "https://paidfor.dev";
|
|
19
19
|
export declare const TOOL_NAMES: readonly ["check_x402_seller", "list_reliable_sellers", "recent_changes", "report_x402_payment"];
|
|
20
|
-
/**
|
|
21
|
-
* The no-account promise, word for word wherever it appears: the server instructions (last line),
|
|
22
|
-
* this package's README, /methodology, /install.md and /privacy on the site.
|
|
23
|
-
*/
|
|
24
|
-
export declare const NO_ACCOUNT_PROMISE: string;
|
|
25
20
|
/** Sent on initialize by every transport (stdio, the hosted service, the site's /mcp route), so clients call the tools unprompted.
|
|
26
21
|
* Generated from rules.ts, like the tool descriptions below and the skill at paidfor.dev/skill.md. */
|
|
27
22
|
export declare const INSTRUCTIONS: string;
|
package/dist/server.js
CHANGED
|
@@ -10,7 +10,7 @@ import { ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTempl
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { CHANGES_TRIGGER, CHECK_TRIGGERS, CHOOSE_TRIGGER, NEVER_PAY_TWICE, REPORT_DESCRIPTION, REPORT_OUTCOMES, REPORT_SENDS, SENDS_ONLY_URL, VERDICT_SENTENCE, cap, listAnd, listOr } from "./rules.js";
|
|
12
12
|
export * from "./rules.js";
|
|
13
|
-
export const VERSION = "0.
|
|
13
|
+
export const VERSION = "0.7.0";
|
|
14
14
|
/** Sent on every request to the index so usage can be counted by channel (never anything else). */
|
|
15
15
|
export const CLIENT_HEADER = `mcp/${VERSION}`;
|
|
16
16
|
/** What the hosted service (index.ts) sends instead, so the index can count hosted and stdio use apart. */
|
|
@@ -35,12 +35,6 @@ export const taggedClientHeader = (tag) => (tag ? `${HOSTED_CLIENT_HEADER}+${tag
|
|
|
35
35
|
export const DEFAULT_BASE = "https://paidfor.dev";
|
|
36
36
|
export const TOOL_NAMES = ["check_x402_seller", "list_reliable_sellers", "recent_changes", "report_x402_payment"];
|
|
37
37
|
const CATEGORIES = ["inference", "data", "scraping", "compute", "other"];
|
|
38
|
-
/**
|
|
39
|
-
* The no-account promise, word for word wherever it appears: the server instructions (last line),
|
|
40
|
-
* this package's README, /methodology, /install.md and /privacy on the site.
|
|
41
|
-
*/
|
|
42
|
-
export const NO_ACCOUNT_PROMISE = "Everything that reads the index or contributes data (the site, the API, the MCP tools, the client, telemetry and the changes feed) needs no account and no key, now and later. " +
|
|
43
|
-
"An email address is asked for only to deliver alerts or to join a waitlist, as a confirmation link, never a profile.";
|
|
44
38
|
/** Sent on initialize by every transport (stdio, the hosted service, the site's /mcp route), so clients call the tools unprompted.
|
|
45
39
|
* Generated from rules.ts, like the tool descriptions below and the skill at paidfor.dev/skill.md. */
|
|
46
40
|
export const INSTRUCTIONS = "paidfor rates x402 sellers by paying them and verifying delivery. " +
|
|
@@ -50,9 +44,7 @@ export const INSTRUCTIONS = "paidfor rates x402 sellers by paying them and verif
|
|
|
50
44
|
`${cap(CHANGES_TRIGGER)}, call recent_changes. ` +
|
|
51
45
|
`${NEVER_PAY_TWICE} ` +
|
|
52
46
|
`${SENDS_ONLY_URL} ` +
|
|
53
|
-
`After any x402 payment made with any wallet outside this server, call report_x402_payment with the resource URL, the settlement transaction hash and the outcome; it moves no money and sends only ${REPORT_SENDS}
|
|
54
|
-
"\n" +
|
|
55
|
-
NO_ACCOUNT_PROMISE;
|
|
47
|
+
`After any x402 payment made with any wallet outside this server, call report_x402_payment with the resource URL, the settlement transaction hash and the outcome; it moves no money and sends only ${REPORT_SENDS}.`;
|
|
56
48
|
/**
|
|
57
49
|
* What the hosted transports (index.ts, the site's /mcp route) send instead of INSTRUCTIONS: the same facts, described
|
|
58
50
|
* rather than directed, because the Connectors Directory review rejects text that tells Claude how to behave or to call
|
|
@@ -64,8 +56,7 @@ export const HOSTED_INSTRUCTIONS = "paidfor rates x402 sellers by paying them an
|
|
|
64
56
|
"list_reliable_sellers returns sellers with zero failures across at least 30 paid calls, for choosing among sellers. " +
|
|
65
57
|
"recent_changes returns what changed in the index, which is diffed hourly. " +
|
|
66
58
|
`${SENDS_ONLY_URL} ` +
|
|
67
|
-
`report_x402_payment records what came back after a payment the user's own wallet already made, from the resource URL, the settlement transaction hash and the outcome; it moves no money, sends only ${REPORT_SENDS}, and the report counts only once the transaction is verified as a USDC transfer on Base
|
|
68
|
-
NO_ACCOUNT_PROMISE;
|
|
59
|
+
`report_x402_payment records what came back after a payment the user's own wallet already made, from the resource URL, the settlement transaction hash and the outcome; it moves no money, sends only ${REPORT_SENDS}, and the report counts only once the transaction is verified as a USDC transfer on Base.`;
|
|
69
60
|
/** The first sentence of each read tool description: when to call it. The manifest and the setup page list these as they are. */
|
|
70
61
|
export const TOOL_TRIGGERS = {
|
|
71
62
|
check_x402_seller: `Call this ${listOr(CHECK_TRIGGERS)}.`,
|
|
@@ -88,7 +79,7 @@ export const SERVER_INFO = {
|
|
|
88
79
|
title: "paidfor: x402 seller reliability",
|
|
89
80
|
version: VERSION,
|
|
90
81
|
description: "Helps an agent decide whether to pay an x402 API seller on Base: verdicts, failure rates and evidence from real paid calls, " +
|
|
91
|
-
"a list of sellers that have never failed, and a feed of recent changes.
|
|
82
|
+
"a list of sellers that have never failed, and a feed of recent changes.",
|
|
92
83
|
websiteUrl: DEFAULT_BASE,
|
|
93
84
|
icons: [{ src: `${DEFAULT_BASE}/icon.png`, mimeType: "image/png", sizes: ["512x512"] }],
|
|
94
85
|
};
|
package/dist/setup.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export declare const MANIFEST_URL = "https://paidfor.dev/.well-known/mcp.json";
|
|
|
6
6
|
export declare const AGENT_SETUP = "Tell your agent to read https://paidfor.dev/skill.md";
|
|
7
7
|
/** What the hosted transports put in X-Paidfor-Client on every call to the index (taggedClientHeader: mcp-hosted/<version>+<client tag>). */
|
|
8
8
|
export declare const CLIENT_HEADER_NOTE = "Every call the server makes to the index carries X-Paidfor-Client: mcp-hosted/<version>+<client>, so use can be counted by channel; <client> is your client product (claude, chatgpt, cursor or other, from its name on initialize), never you.";
|
|
9
|
+
/** The one-command install (`paidfor-mcp init`): detects the clients, offers a wallet and a link to an account, writes the config. */
|
|
10
|
+
export declare const INIT_COMMAND: (pkg: string) => string;
|
|
11
|
+
/** The Claude Code command that adds the hosted server, user scope. */
|
|
9
12
|
export declare const claudeCodeCommand: (url: string) => string;
|
|
10
13
|
export declare const cursorServer: (url: string) => {
|
|
11
14
|
url: string;
|
|
@@ -43,6 +46,5 @@ export declare const MANIFEST_META: {
|
|
|
43
46
|
skill_url: string;
|
|
44
47
|
privacy_url: string;
|
|
45
48
|
auth: string;
|
|
46
|
-
no_account: string;
|
|
47
49
|
};
|
|
48
50
|
};
|
package/dist/setup.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* /install), the HTML page a browser gets from GET on the MCP URL, and the `_meta` block of the manifest at
|
|
4
4
|
* paidfor.dev/.well-known/mcp.json. All of it is generated from server.ts and rules.ts, like the tool descriptions.
|
|
5
5
|
*/
|
|
6
|
-
import { DEFAULT_BASE, HOSTED_INSTRUCTIONS,
|
|
6
|
+
import { DEFAULT_BASE, HOSTED_INSTRUCTIONS, TOOL_NAMES, TOOL_TRIGGERS, VERSION } from "./server.js";
|
|
7
7
|
export const HOSTED_URL = "https://mcp.paidfor.dev/mcp";
|
|
8
8
|
export const SETUP_URL = `${DEFAULT_BASE}/install`;
|
|
9
9
|
export const SKILL_URL = `${DEFAULT_BASE}/skill.md`;
|
|
@@ -12,6 +12,9 @@ export const MANIFEST_URL = `${DEFAULT_BASE}/.well-known/mcp.json`;
|
|
|
12
12
|
export const AGENT_SETUP = `Tell your agent to read ${SKILL_URL}`;
|
|
13
13
|
/** What the hosted transports put in X-Paidfor-Client on every call to the index (taggedClientHeader: mcp-hosted/<version>+<client tag>). */
|
|
14
14
|
export const CLIENT_HEADER_NOTE = "Every call the server makes to the index carries X-Paidfor-Client: mcp-hosted/<version>+<client>, so use can be counted by channel; <client> is your client product (claude, chatgpt, cursor or other, from its name on initialize), never you.";
|
|
15
|
+
/** The one-command install (`paidfor-mcp init`): detects the clients, offers a wallet and a link to an account, writes the config. */
|
|
16
|
+
export const INIT_COMMAND = (pkg) => `npx -y ${pkg} init`;
|
|
17
|
+
/** The Claude Code command that adds the hosted server, user scope. */
|
|
15
18
|
export const claudeCodeCommand = (url) => `claude mcp add --transport http --scope user paidfor ${url}`;
|
|
16
19
|
export const cursorServer = (url) => ({ url });
|
|
17
20
|
export const cursorDeepLink = (url) => `cursor://anysphere.cursor-deeplink/mcp/install?name=paidfor&config=${encodeURIComponent(Buffer.from(JSON.stringify(cursorServer(url)), "utf8").toString("base64"))}`;
|
|
@@ -57,7 +60,7 @@ export function setupPageHtml({ endpoint, statusUrl }) {
|
|
|
57
60
|
return `<!doctype html>
|
|
58
61
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
59
62
|
<title>paidfor MCP server</title>
|
|
60
|
-
<meta name="description" content="Set up the paidfor MCP server: check an x402 seller before your agent pays.
|
|
63
|
+
<meta name="description" content="Set up the paidfor MCP server: check an x402 seller before your agent pays.">
|
|
61
64
|
<link rel="icon" href="${DEFAULT_BASE}/icon.png"><link rel="alternate" type="application/json" href="${esc(MANIFEST_URL)}">
|
|
62
65
|
<style>${CSS}</style></head>
|
|
63
66
|
<body><main>
|
|
@@ -91,8 +94,6 @@ ${pre(cursorConfig(HOSTED_URL))}
|
|
|
91
94
|
<ul>${tools}</ul>
|
|
92
95
|
<details><summary>Server instructions, as sent on initialize</summary><p>${esc(HOSTED_INSTRUCTIONS)}</p></details>
|
|
93
96
|
|
|
94
|
-
<h2>No account, no key</h2>
|
|
95
|
-
<p>${esc(NO_ACCOUNT_PROMISE)}</p>
|
|
96
97
|
<p class="muted">${esc(CLIENT_HEADER_NOTE)}</p>
|
|
97
98
|
|
|
98
99
|
<footer class="muted"><a href="${MANIFEST_URL}">Manifest</a> · <a href="${DEFAULT_BASE}/api">API</a> · <a href="${PRIVACY_URL}">Privacy</a> · <a href="${esc(statusUrl)}">Status JSON</a></footer>
|
|
@@ -113,6 +114,5 @@ export const MANIFEST_META = {
|
|
|
113
114
|
skill_url: SKILL_URL,
|
|
114
115
|
privacy_url: PRIVACY_URL,
|
|
115
116
|
auth: "none",
|
|
116
|
-
no_account: NO_ACCOUNT_PROMISE,
|
|
117
117
|
},
|
|
118
118
|
};
|
package/dist/stdio.js
CHANGED
|
@@ -2,14 +2,28 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* `npx @paidfor/mcp`: the stdio server. With PAIDFOR_WALLET_KEY_FILE (or PAIDFOR_WALLET_KEY) set it also
|
|
4
4
|
* registers pay_x402 and wallet_status; without one it is the same read-only server as the hosted one.
|
|
5
|
+
* `paidfor-mcp init` does the whole install: finds the MCP clients, offers a wallet and a link to an account, writes the config.
|
|
5
6
|
* `paidfor-mcp wallet new` writes a fresh key to ~/.paidfor/wallet.key (mode 600) and prints the address.
|
|
6
|
-
* `paidfor-mcp wallet register` proves that wallet to a paidfor team (PAIDFOR_TEAM_KEY)
|
|
7
|
+
* `paidfor-mcp wallet register` proves that wallet to a paidfor team (PAIDFOR_TEAM_KEY). With PAIDFOR_TEAM_KEY also set on
|
|
8
|
+
* the server, pay_x402 asks the team's rules before every payment.
|
|
7
9
|
*/
|
|
8
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
11
|
import { DEFAULT_BASE, createServer } from "./server.js";
|
|
10
12
|
import { FUND_SENTENCE, WalletConfigError, loadWallet, newWallet, readLimits, registerWallet } from "./wallet.js";
|
|
11
13
|
const [cmd, sub, ...rest] = process.argv.slice(2);
|
|
12
14
|
const baseUrl = (process.env.PAIDFOR_API_BASE ?? process.env.PAIDFOR_URL ?? DEFAULT_BASE).replace(/\/$/, "");
|
|
15
|
+
if (cmd === "init" || cmd === "link") {
|
|
16
|
+
// The one-command install (init.ts); `link` is its link, register and config steps alone.
|
|
17
|
+
const { realDeps, runInit } = await import("./init.js");
|
|
18
|
+
const args = process.argv.slice(3);
|
|
19
|
+
try {
|
|
20
|
+
process.exit(await runInit(args, realDeps(), cmd));
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
process.stderr.write(`paidfor-mcp: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
13
27
|
if (cmd === "wallet") {
|
|
14
28
|
const usage = "usage: paidfor-mcp wallet new | PAIDFOR_TEAM_KEY=pft_... paidfor-mcp wallet register [--label \"research agent\"]\n";
|
|
15
29
|
try {
|
|
@@ -63,7 +77,9 @@ if (pay && limits) {
|
|
|
63
77
|
const { INSTRUCTIONS_WITH_PAY, registerPayTools } = await import("./pay.js");
|
|
64
78
|
const server = createServer({ baseUrl, instructions: INSTRUCTIONS_WITH_PAY });
|
|
65
79
|
// PAIDFOR_FETCH_ALIAS=1 also registers the handler as fetch_paid_url (eval/routing.mjs measures whether the second name helps).
|
|
66
|
-
|
|
80
|
+
// PAIDFOR_TEAM_KEY: every payment asks the team's rules first (docs/team-api.md).
|
|
81
|
+
const teamKey = process.env.PAIDFOR_TEAM_KEY?.trim() || undefined;
|
|
82
|
+
await registerPayTools(server, { wallet: pay, limits, baseUrl, fetchAlias: process.env.PAIDFOR_FETCH_ALIAS === "1", ...(teamKey ? { teamKey } : {}) });
|
|
67
83
|
await server.connect(new StdioServerTransport());
|
|
68
84
|
}
|
|
69
85
|
else {
|