@tempoxyz/patron 0.0.26 → 0.0.28

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 CHANGED
@@ -6,13 +6,13 @@ Your AI agents pay every website they read: $0.001 a page, in OUSD on Tempo, pri
6
6
  npx @tempoxyz/patron
7
7
  ```
8
8
 
9
- First run signs you in with Tempo Wallet, asks for a monthly budget (default $5), buys
10
- seals and adds a hook to every supported agent it finds. After that, every page an agent
11
- reads pays its site one seal. Nothing stays running: the hook hands each read to a one-shot
12
- background payment and exits at once, so the agent never waits, and when seals run low it
9
+ First run signs you in with Tempo Wallet, asks for a monthly budget (default $5), adds
10
+ funds to your Patron balance and adds a hook to every supported agent it finds. After that, every page an agent
11
+ reads pays its site $0.001. Nothing stays running: the hook hands each read to a one-shot
12
+ background payment and exits at once, so the agent never waits, and when your balance runs low it
13
13
  starts a one-shot purchase that exits when done.
14
14
 
15
- Run `npx @tempoxyz/patron` again for status (seals left, pages paid, budget used, rank) and to pause,
15
+ Run `npx @tempoxyz/patron` again for status (balance, pages paid, tips, budget used, rank) and to pause,
16
16
  change the budget or uninstall.
17
17
 
18
18
  ## Commands
@@ -20,7 +20,7 @@ change the budget or uninstall.
20
20
  | Command | What it does |
21
21
  | :- | :- |
22
22
  | `patron` | Set up, or show status and options |
23
- | `patron status` | Seals left, pages paid this month, budget used, leaderboard rank |
23
+ | `patron status` | Balance, pages paid and tips this month, budget used, leaderboard rank |
24
24
  | `patron pause` / `patron resume` | Stop or restart paying |
25
25
  | `patron budget <usd>` | Change the monthly budget |
26
26
  | `patron tip <url> [usd]` | Tip a site, $0.10 by default |
@@ -34,7 +34,7 @@ change the budget or uninstall.
34
34
  - A fetched page pays its site only when it loads (a 2xx status). Error pages, blocks and
35
35
  redirect notices pay nothing.
36
36
  - A search pays each site in its results, up to 10 sites per search.
37
- - Each site's seals are redeemed at once, in separate relayed requests, never batched: the
37
+ - Each site's payments are redeemed at once, in separate relayed requests, never batched: the
38
38
  gateway cannot tell which sites one reader read together.
39
39
  - Sites are paid per registrable domain: `docs.python.org` pays `python.org`. Authors on shared
40
40
  hosts are paid as their own site: `alice.github.io`, `app.vercel.app`, `alice.substack.com`.
@@ -44,19 +44,19 @@ change the budget or uninstall.
44
44
 
45
45
  ## Privacy
46
46
 
47
- Seals are Privacy Pass blind tokens (RFC 9578). The gateway knows your address bought a
48
- number of seals, but cannot link a seal to you when it is spent. Each read sends one seal
47
+ Payments are Privacy Pass blind tokens (RFC 9578). The gateway knows how much your address
48
+ added to its balance, but cannot link a payment to you when it is spent. Each read sends one payment
49
49
  and the site's domain through an Oblivious HTTP relay (RFC 9458): the relay sees your IP but
50
50
  not the content; the gateway sees the content but not your IP.
51
51
 
52
- Local state lives in `~/.patron`: settings, seals encrypted with AES-256-GCM under a key
52
+ Local state lives in `~/.patron`: settings, your balance's blind tokens encrypted with AES-256-GCM under a key
53
53
  file readable only by you, this month's per-site counts, which never leave the machine, and
54
54
  a copy of the CLI in `~/.patron/cli` that agent hooks start with node, so a read never waits
55
55
  on npx. Running `npx @tempoxyz/patron` updates the copy.
56
56
 
57
57
  ## Budget
58
58
 
59
- The budget caps purchases: Patron never buys more than the monthly budget's worth of seals
59
+ The budget caps purchases: Patron never buys more than the monthly budget
60
60
  in a calendar month. With Tempo Wallet, the access key Patron holds also has a spending
61
61
  limit equal to the budget, renewed every 30 days.
62
62
 
package/dist/bin.js CHANGED
@@ -4,14 +4,14 @@ const require = __patronCreateRequire(import.meta.url)
4
4
 
5
5
  // src/bin.ts
6
6
  if (process.argv[2] === "hook") {
7
- const { runHookProcess } = await import("./chunks/hook-process-ZPEWRFL2.js");
7
+ const { runHookProcess } = await import("./chunks/hook-process-IMQMGXBT.js");
8
8
  await runHookProcess().catch(() => void 0);
9
9
  process.exit(0);
10
10
  } else if (process.argv[2] === "pay") {
11
- const { runPayProcess } = await import("./chunks/hook-process-ZPEWRFL2.js");
11
+ const { runPayProcess } = await import("./chunks/hook-process-IMQMGXBT.js");
12
12
  await runPayProcess().catch(() => void 0);
13
13
  process.exit(0);
14
14
  } else {
15
- const { run } = await import("./chunks/-WOZ4DM2Y.js");
15
+ const { run } = await import("./chunks/-5Q7V7W7W.js");
16
16
  process.exitCode = await run(process.argv.slice(2));
17
17
  }
@@ -3,8 +3,6 @@ const require = __patronCreateRequire(import.meta.url)
3
3
  import {
4
4
  FileTokenStore,
5
5
  LINK_TOKEN_STEP,
6
- LeaderboardResponse,
7
- PatronInfo,
8
6
  STARTER_TOKENS,
9
7
  budgetTokens,
10
8
  buyTokens,
@@ -17,7 +15,6 @@ import {
17
15
  loadIssuer,
18
16
  needsRefill,
19
17
  parseUsd,
20
- profileMessage,
21
18
  purchasableTokens,
22
19
  readJsonObject,
23
20
  redeem,
@@ -29,26 +26,20 @@ import {
29
26
  withPatronStatus,
30
27
  withoutPatronHandlers,
31
28
  writeJsonObject
32
- } from "./chunk-NQ6RRPM6.js";
29
+ } from "./chunk-EKFENOVR.js";
33
30
  import {
34
31
  startRefill
35
32
  } from "./chunk-TV5VLFTD.js";
36
33
  import {
37
- DEFAULT_MONTHLY_BUDGET_USD,
38
- MAX_TOKENS_PER_PAGE,
39
- MIN_TOKENS_PER_PAGE,
40
- PACK_SIZE,
41
- TOKEN_PRICE_UNITS,
42
- clampTokensPerPage,
43
- formatUsd,
44
- normalizePayee,
45
34
  resolveSettings,
46
- settingsFor,
47
- tokensToUnits
48
- } from "./chunk-WZRGCKEK.js";
35
+ settingsFor
36
+ } from "./chunk-OQXACACU.js";
49
37
  import {
50
38
  AGENT_IDS,
39
+ LeaderboardResponse,
40
+ PatronInfo,
51
41
  ensureHome,
42
+ profileMessage,
52
43
  readConfig,
53
44
  readJson,
54
45
  readUsage,
@@ -56,11 +47,20 @@ import {
56
47
  updateUsage,
57
48
  writeConfig,
58
49
  writePrivateFile
59
- } from "./chunk-SGQ525FB.js";
50
+ } from "./chunk-EV7OA6GK.js";
60
51
  import {
52
+ DEFAULT_MONTHLY_BUDGET_USD,
53
+ MAX_TOKENS_PER_PAGE,
54
+ MIN_TOKENS_PER_PAGE,
55
+ PACK_SIZE,
56
+ TOKEN_PRICE_UNITS,
57
+ clampTokensPerPage,
58
+ formatUsd,
59
+ normalizePayee,
60
+ tokensToUnits,
61
61
  tryLock,
62
62
  withLock
63
- } from "./chunk-O64T22EK.js";
63
+ } from "./chunk-EISHX7Q3.js";
64
64
  import "./chunk-GTMLTZRN.js";
65
65
 
66
66
  // src/index.ts
@@ -700,10 +700,10 @@ async function linkRefillFailedAt(home) {
700
700
  var REFILL_LOCK_STALE_MS = 5 * 60 * 1e3;
701
701
  var defaultOpenWallet = async (settings, config, issuer, maxUnits) => {
702
702
  if (config.wallet === "link") {
703
- const { openLinkWallet } = await import("./wallet-IDSQUCB5.js");
703
+ const { openLinkWallet } = await import("./wallet-6TDMXRWW.js");
704
704
  return openLinkWallet(config, maxUnits);
705
705
  }
706
- const { openWallet } = await import("./wallet-IDSQUCB5.js");
706
+ const { openWallet } = await import("./wallet-6TDMXRWW.js");
707
707
  return openWallet(settings, config.wallet, issuer, maxUnits);
708
708
  };
709
709
  async function buyWithinBudget(settings, config, wanted, openWallet = defaultOpenWallet) {
@@ -778,17 +778,14 @@ function formatStatus(view, color) {
778
778
  const top = Object.entries(usage.domains).sort(([, left], [, right]) => right - left).slice(0, 3).map(([domain, count]) => `${domain} ${count}`);
779
779
  const lines = [
780
780
  `${styleText("Patron", "bold", color)} ${state}${network}`,
781
- row(
782
- "Seals left",
783
- `${tokensLeft.toLocaleString("en-US")} (${tokensUsd(tokensLeft)})`,
784
- color
785
- ),
781
+ row("Balance", `${tokensUsd(tokensLeft)} left`, color),
786
782
  row(
787
783
  "Per page",
788
784
  `$${(clampTokensPerPage(config.tokensPerPage) / 1e3).toFixed(3)}`,
789
785
  color
790
786
  ),
791
787
  row("Pages paid", `${usage.paidPages.toLocaleString("en-US")} this month`, color),
788
+ row("Tipped", `${tokensUsd(usage.tippedTokens)} this month`, color),
792
789
  row(
793
790
  "Budget used",
794
791
  `${bought} of ${budget} this month (${budgetTokens(config.monthlyBudgetUsd) > 0 ? Math.min(100, Math.round(usage.boughtTokens / budgetTokens(config.monthlyBudgetUsd) * 100)) : 0}%)`,
@@ -796,7 +793,7 @@ function formatStatus(view, color) {
796
793
  ),
797
794
  row(
798
795
  "Rank",
799
- standing?.rank ? `#${standing.rank} \xB7 ${standing.tokens.toLocaleString("en-US")} seals bought` : styleText("not ranked yet", "dim", color),
796
+ standing?.rank ? `#${standing.rank} \xB7 ${tokensUsd(standing.tokens)} added` : styleText("not ranked yet", "dim", color),
800
797
  color
801
798
  ),
802
799
  row(
@@ -835,7 +832,7 @@ var HELP = `patron@${VERSION} \u2014 pay the websites your agents read, privatel
835
832
  Usage: patron [command]
836
833
 
837
834
  (none) Set up, or show status and options
838
- status Seals left, pages paid, budget and rank
835
+ status Balance, pages paid, tips, budget and rank
839
836
  pause | resume Stop or restart paying
840
837
  budget <usd> Change the monthly budget
841
838
  per-page <usd> What each site gets per page read, $0.001 to $0.01
@@ -864,6 +861,19 @@ function defaults() {
864
861
  stdout: process.stdout
865
862
  };
866
863
  }
864
+ var REFRESHES_STABLE_COPY = /* @__PURE__ */ new Set([
865
+ "status",
866
+ "pause",
867
+ "resume",
868
+ "budget",
869
+ "per-page",
870
+ "tip",
871
+ "name"
872
+ ]);
873
+ async function refreshStableCli(deps) {
874
+ const config = await readConfig(deps.settings.home).catch(() => void 0);
875
+ if (config) await installStableCli(deps.settings.home).catch(() => void 0);
876
+ }
867
877
  async function run(argv, overrides = {}) {
868
878
  const base = { ...defaults(), ...overrides };
869
879
  let shared;
@@ -876,6 +886,8 @@ async function run(argv, overrides = {}) {
876
886
  };
877
887
  const [command, ...args] = argv;
878
888
  try {
889
+ if (command !== void 0 && REFRESHES_STABLE_COPY.has(command))
890
+ await refreshStableCli(deps);
879
891
  switch (command) {
880
892
  case void 0:
881
893
  await main(deps);
@@ -1022,9 +1034,9 @@ async function setup(deps) {
1022
1034
  await writeConfig(settings.home, config);
1023
1035
  const starter = config.wallet === "link" ? PACK_SIZE : STARTER_TOKENS;
1024
1036
  const first = purchasableTokens(config, await readUsage(settings.home), starter);
1025
- step(deps, `Buying ${first.toLocaleString("en-US")} seals (${tokensUsd(first)})`);
1037
+ step(deps, `Adding ${tokensUsd(first)} to your balance`);
1026
1038
  const bought = await buyWithinBudget(settings, config, starter, deps.openWallet);
1027
- done(deps, `${bought.toLocaleString("en-US")} seals ready`);
1039
+ done(deps, `${tokensUsd(bought)} ready to pay sites`);
1028
1040
  deps.startRefill();
1029
1041
  await installStableCli(settings.home).catch(() => void 0);
1030
1042
  const agents = await detectAgents(deps.agents);
@@ -1072,7 +1084,7 @@ async function tempoSignIn(deps, budgetUnits, issuer) {
1072
1084
  deps,
1073
1085
  deps.settings.privateKey ? "Using PATRON_PRIVATE_KEY" : "Signing in with Tempo Wallet"
1074
1086
  );
1075
- const { signIn } = await import("./wallet-IDSQUCB5.js");
1087
+ const { signIn } = await import("./wallet-6TDMXRWW.js");
1076
1088
  const account = await signIn(deps.settings, {
1077
1089
  budgetUnits,
1078
1090
  issuer,
@@ -1083,7 +1095,7 @@ async function tempoSignIn(deps, budgetUnits, issuer) {
1083
1095
  }
1084
1096
  async function linkSignIn(deps) {
1085
1097
  step(deps, "Checking your Link wallet (link-cli)");
1086
- const { linkBalance, linkCli, linkPaymentMethods } = await import("./link-OU7W7IT6.js");
1098
+ const { linkBalance, linkCli, linkPaymentMethods } = await import("./link-VK7VCE2R.js");
1087
1099
  const cli = deps.linkCli ?? linkCli();
1088
1100
  const methods = await linkPaymentMethods(cli).catch((error) => {
1089
1101
  throw new Error(
@@ -1184,9 +1196,9 @@ async function changeBudget(deps, input) {
1184
1196
  if (budget === void 0 || budget < 1e-3)
1185
1197
  throw new UsageError("Usage: patron budget <usd>, such as patron budget 10");
1186
1198
  } else budget = await askBudget(deps, config.monthlyBudgetUsd);
1187
- if (config.wallet === "tempo" && budget > config.monthlyBudgetUsd) {
1199
+ if (config.wallet === "tempo" && budget >= config.monthlyBudgetUsd) {
1188
1200
  step(deps, "Approve the new spending limit in Tempo Wallet");
1189
- const { authorizeBudget } = await import("./wallet-IDSQUCB5.js");
1201
+ const { authorizeBudget } = await import("./wallet-6TDMXRWW.js");
1190
1202
  const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1191
1203
  fetch: deps.fetch
1192
1204
  });
@@ -1240,8 +1252,9 @@ async function tip(deps, url, amount = "0.10") {
1240
1252
  throw new Error(
1241
1253
  `This tip needs ${tokensUsd(missing)} more than this month's budget allows. Raise it with patron budget <usd>.`
1242
1254
  );
1243
- step(deps, `Buying ${missing.toLocaleString("en-US")} seals (${tokensUsd(missing)})`);
1244
- await buyWithinBudget(settings, config, missing, deps.openWallet);
1255
+ const amount2 = Math.max(missing, purchasableTokens(config, usage, PACK_SIZE));
1256
+ step(deps, `Adding ${tokensUsd(amount2)} to your balance`);
1257
+ await buyWithinBudget(settings, config, amount2, deps.openWallet);
1245
1258
  }
1246
1259
  const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1247
1260
  fetch: deps.fetch
@@ -1275,16 +1288,17 @@ async function tip(deps, url, amount = "0.10") {
1275
1288
  if (paid > 0)
1276
1289
  await updateUsage(settings.home, (usage) => {
1277
1290
  usage.domains[domain] = (usage.domains[domain] ?? 0) + paid;
1291
+ usage.tippedTokens += paid;
1278
1292
  });
1279
1293
  if (paid === 0)
1280
1294
  throw new Error(
1281
- rejection ? `Patron rejected the tip to ${domain}: ${rejection}.` : `Could not reach Patron to tip ${domain}; your seals were kept.`
1295
+ rejection ? `Patron rejected the tip to ${domain}: ${rejection}.` : `Could not reach Patron to tip ${domain}; your balance was kept.`
1282
1296
  );
1283
1297
  if (paid < count)
1284
1298
  say(
1285
1299
  deps,
1286
1300
  styleText(
1287
- ` ${count - paid} of ${count} seals did not go through.`,
1301
+ ` ${tokensUsd(count - paid)} of ${tokensUsd(count)} did not go through.`,
1288
1302
  "yellow",
1289
1303
  deps.color
1290
1304
  )
@@ -1302,7 +1316,7 @@ async function setName(deps, name) {
1302
1316
  const issuer = await loadIssuer(settings.home, settings.gatewayUrl, {
1303
1317
  fetch: deps.fetch
1304
1318
  });
1305
- const open = deps.openWallet ?? (async (s, c, i, max) => (await import("./wallet-IDSQUCB5.js")).openWallet(s, c.wallet, i, max));
1319
+ const open = deps.openWallet ?? (async (s, c, i, max) => (await import("./wallet-6TDMXRWW.js")).openWallet(s, c.wallet, i, max));
1306
1320
  const wallet = await open(settings, config, issuer, 0n);
1307
1321
  await setProfile(settings.gatewayUrl, wallet, name, deps.fetch);
1308
1322
  await writeConfig(settings.home, { ...config, name });
@@ -1325,7 +1339,7 @@ async function uninstall(deps) {
1325
1339
  const prompter = deps.prompter();
1326
1340
  try {
1327
1341
  purge = await prompter.confirm(
1328
- `Also delete ${deps.settings.home}${tokensLeft > 0 ? ` and ${tokensLeft} unspent seals (${tokensUsd(tokensLeft)})` : ""}?`,
1342
+ `Also delete ${deps.settings.home}${tokensLeft > 0 ? ` and your unspent balance of ${tokensUsd(tokensLeft)}` : ""}?`,
1329
1343
  false
1330
1344
  );
1331
1345
  } finally {
@@ -797,6 +797,10 @@ var TOKEN_PRICE_UNITS = 1000n;
797
797
  var PACK_SIZE = 1e3;
798
798
  var MAX_TOKENS_PER_PURCHASE = 5e3;
799
799
  var DEFAULT_MONTHLY_BUDGET_USD = 5;
800
+ function accessKeyLimitUnits(budgetUnits) {
801
+ if (budgetUnits <= 0n) return 0n;
802
+ return budgetUnits + 50000n + budgetUnits / 100n;
803
+ }
800
804
  var MIN_TOKENS_PER_PAGE = 1;
801
805
  var MAX_TOKENS_PER_PAGE = 10;
802
806
  var DEFAULT_TOKENS_PER_PAGE = 1;
@@ -956,36 +960,6 @@ function normalizePayee(input) {
956
960
  return normalizeDomain(trimmed);
957
961
  }
958
962
 
959
- // src/settings.ts
960
- import { homedir } from "node:os";
961
- import { join } from "node:path";
962
- var DEFAULT_GATEWAY_URL = "https://patron-gateway.tempo-dev.workers.dev";
963
- var DEFAULT_RELAY_URL = "https://patron-relay.tempo-dev.workers.dev";
964
- function resolveSettings(env = process.env) {
965
- const key = env.PATRON_PRIVATE_KEY?.trim();
966
- const privateKey = key && /^0x[0-9a-fA-F]{64}$/.test(key) ? key : void 0;
967
- return {
968
- gatewayUrl: env.PATRON_GATEWAY_URL || DEFAULT_GATEWAY_URL,
969
- home: env.PATRON_HOME || join(homedir(), ".patron"),
970
- network: env.PATRON_NETWORK === "testnet" ? "testnet" : "mainnet",
971
- overridden: {
972
- gatewayUrl: Boolean(env.PATRON_GATEWAY_URL),
973
- network: Boolean(env.PATRON_NETWORK),
974
- relayUrl: Boolean(env.PATRON_RELAY_URL)
975
- },
976
- relayUrl: env.PATRON_RELAY_URL || DEFAULT_RELAY_URL,
977
- ...privateKey ? { privateKey } : {}
978
- };
979
- }
980
- function settingsFor(settings, saved) {
981
- return {
982
- ...settings,
983
- gatewayUrl: settings.overridden.gatewayUrl || !saved.gatewayUrl ? settings.gatewayUrl : saved.gatewayUrl,
984
- network: settings.overridden.network ? settings.network : saved.network,
985
- relayUrl: settings.overridden.relayUrl || !saved.relayUrl ? settings.relayUrl : saved.relayUrl
986
- };
987
- }
988
-
989
963
  // ../protocol/src/encoding.ts
990
964
  function toBase64Url(bytes) {
991
965
  let binary = "";
@@ -1190,11 +1164,59 @@ function tokensToUnits(tokens) {
1190
1164
  return BigInt(tokens) * TOKEN_PRICE_UNITS;
1191
1165
  }
1192
1166
 
1167
+ // src/lock.ts
1168
+ import { open, rm, stat } from "node:fs/promises";
1169
+ async function withLock(path, fn, options = {}) {
1170
+ const deadline = Date.now() + (options.timeoutMs ?? 3e3);
1171
+ const staleMs = options.staleMs ?? 1e4;
1172
+ for (; ; ) {
1173
+ try {
1174
+ const handle = await open(path, "wx", 384);
1175
+ await handle.close();
1176
+ break;
1177
+ } catch (error) {
1178
+ if (error.code !== "EEXIST") throw error;
1179
+ const age = await stat(path).then(
1180
+ (stats) => Date.now() - stats.mtimeMs,
1181
+ () => 0
1182
+ );
1183
+ if (age > staleMs) {
1184
+ await rm(path, { force: true });
1185
+ continue;
1186
+ }
1187
+ if (Date.now() > deadline) throw new Error(`Patron state is busy: ${path}`);
1188
+ await new Promise((resolve) => setTimeout(resolve, 10 + Math.random() * 20));
1189
+ }
1190
+ }
1191
+ try {
1192
+ return await fn();
1193
+ } finally {
1194
+ await rm(path, { force: true });
1195
+ }
1196
+ }
1197
+ async function tryLock(path, staleMs) {
1198
+ try {
1199
+ const handle = await open(path, "wx", 384);
1200
+ await handle.close();
1201
+ } catch (error) {
1202
+ if (error.code !== "EEXIST") throw error;
1203
+ const age = await stat(path).then(
1204
+ (stats) => Date.now() - stats.mtimeMs,
1205
+ () => 0
1206
+ );
1207
+ if (age <= staleMs) return void 0;
1208
+ await rm(path, { force: true });
1209
+ return tryLock(path, staleMs);
1210
+ }
1211
+ return () => rm(path, { force: true });
1212
+ }
1213
+
1193
1214
  export {
1194
1215
  TOKEN_PRICE_UNITS,
1195
1216
  PACK_SIZE,
1196
1217
  MAX_TOKENS_PER_PURCHASE,
1197
1218
  DEFAULT_MONTHLY_BUDGET_USD,
1219
+ accessKeyLimitUnits,
1198
1220
  MIN_TOKENS_PER_PAGE,
1199
1221
  MAX_TOKENS_PER_PAGE,
1200
1222
  clampTokensPerPage,
@@ -1206,6 +1228,6 @@ export {
1206
1228
  isExcludedByDefault,
1207
1229
  formatUsd,
1208
1230
  tokensToUnits,
1209
- resolveSettings,
1210
- settingsFor
1231
+ withLock,
1232
+ tryLock
1211
1233
  };