@tempoxyz/patron 0.0.16 → 0.0.17

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
@@ -7,9 +7,10 @@ npx @tempoxyz/patron
7
7
  ```
8
8
 
9
9
  First run signs you in with Tempo Wallet, asks for a monthly budget (default $5), buys
10
- seals and adds a hook to Claude Code. After that, every page Claude Code fetches pays its
11
- site one seal. Nothing stays running: the hook runs once per read and exits, and when
12
- seals run low it starts a one-shot purchase that exits when done.
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
13
+ starts a one-shot purchase that exits when done.
13
14
 
14
15
  Run `npx @tempoxyz/patron` again for status (seals left, pages paid, budget used, rank) and to pause,
15
16
  change the budget or uninstall.
@@ -24,9 +25,23 @@ change the budget or uninstall.
24
25
  | `patron budget <usd>` | Change the monthly budget |
25
26
  | `patron tip <url> [usd]` | Tip a site, $0.10 by default |
26
27
  | `patron name <handle>` | Set your public leaderboard name |
28
+ | `patron per-page <usd>` | What each site gets per page read, $0.001 to $0.01 |
27
29
  | `patron uninstall` | Remove agent hooks, optionally local data |
28
30
  | `patron hook` | Run by agents after each web read |
29
31
 
32
+ ## What gets paid
33
+
34
+ - A fetched page pays its site only when it loads (a 2xx status). Error pages, blocks and
35
+ redirect notices pay nothing.
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
38
+ gateway cannot tell which sites one reader read together.
39
+ - Sites are paid per registrable domain: `docs.python.org` pays `python.org`. Authors on shared
40
+ hosts are paid as their own site: `alice.github.io`, `app.vercel.app`, `alice.substack.com`.
41
+ X profiles and posts pay the profile.
42
+ - Never paid: mail, chat and private documents (Gmail, Google Docs, Slack, WhatsApp), sign-in
43
+ pages, banking, health portals, link shorteners (`t.co`, `bit.ly`), localhost and IPs.
44
+
30
45
  ## Privacy
31
46
 
32
47
  Seals are Privacy Pass blind tokens (RFC 9578). The gateway knows your address bought a
@@ -48,9 +63,9 @@ limit equal to the budget, renewed every 30 days.
48
63
  | Agent | Hook |
49
64
  | :- | :- |
50
65
  | Claude Code | Async `PostToolUse` hook for WebFetch and WebSearch in `~/.claude/settings.json` |
51
-
52
- Codex runs web search as a hosted tool that hooks do not see, and Cursor and Amp have no
53
- documented hook for web reads, so they are not supported yet.
66
+ | Cursor | `postToolUse` hook for WebFetch and WebSearch in `~/.cursor/hooks.json` |
67
+ | Codex | `notify` and a `Stop` hook; pays the sites in the session rollout at the end of each turn |
68
+ | Amp | Plugin in `~/.config/amp/plugins/patron.ts` for `read_web_page` and `web_search` |
54
69
 
55
70
  ## Environment
56
71
 
package/dist/bin.js CHANGED
@@ -4,10 +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-GKUDU2B3.js");
7
+ const { runHookProcess } = await import("./chunks/hook-process-WAFRLISI.js");
8
8
  await runHookProcess().catch(() => void 0);
9
9
  process.exit(0);
10
+ } else if (process.argv[2] === "pay") {
11
+ const { runPayProcess } = await import("./chunks/hook-process-WAFRLISI.js");
12
+ await runPayProcess().catch(() => void 0);
13
+ process.exit(0);
10
14
  } else {
11
- const { run } = await import("./chunks/-UIKBA3CN.js");
15
+ const { run } = await import("./chunks/-4YBTFSTL.js");
12
16
  process.exitCode = await run(process.argv.slice(2));
13
17
  }
@@ -40,7 +40,7 @@ import {
40
40
  withPatronStatus,
41
41
  withoutPatronHandlers,
42
42
  writeJsonObject
43
- } from "./chunk-4Z6ZIXMK.js";
43
+ } from "./chunk-GYL4GCNU.js";
44
44
  import {
45
45
  AGENT_IDS,
46
46
  ensureHome,
@@ -8589,6 +8589,30 @@ var TOKEN_TYPE = 2;
8589
8589
 
8590
8590
  // ../protocol/src/domain.ts
8591
8591
  var import_tldts = __toESM(require_cjs(), 1);
8592
+ var AUTHOR_PLATFORMS = /* @__PURE__ */ new Set([
8593
+ "ghost.io",
8594
+ "glitch.me",
8595
+ "hashnode.dev",
8596
+ "medium.com",
8597
+ "micro.blog",
8598
+ "neocities.org",
8599
+ "substack.com",
8600
+ "tumblr.com",
8601
+ "typepad.com",
8602
+ "wordpress.com"
8603
+ ]);
8604
+ var PLATFORM_LABELS = /* @__PURE__ */ new Set([
8605
+ "api",
8606
+ "app",
8607
+ "cdn",
8608
+ "docs",
8609
+ "help",
8610
+ "m",
8611
+ "mobile",
8612
+ "status",
8613
+ "support",
8614
+ "www"
8615
+ ]);
8592
8616
  function normalizeDomain(input2) {
8593
8617
  const trimmed = input2.trim();
8594
8618
  if (!trimmed) return null;
@@ -8603,9 +8627,14 @@ function normalizeDomain(input2) {
8603
8627
  const hostname3 = url2.hostname.replace(/\.$/, "").toLowerCase();
8604
8628
  if (!hostname3 || hostname3 === "localhost" || hostname3.endsWith(".localhost"))
8605
8629
  return null;
8606
- const parsed = (0, import_tldts.parse)(hostname3);
8607
- if (parsed.isIp || !parsed.domain || !parsed.isIcann) return null;
8608
- return parsed.domain;
8630
+ const parsed = (0, import_tldts.parse)(hostname3, { allowPrivateDomains: true });
8631
+ if (parsed.isIp || !parsed.domain || !(parsed.isIcann || parsed.isPrivate)) return null;
8632
+ return authorSite(hostname3, parsed.domain) ?? parsed.domain;
8633
+ }
8634
+ function authorSite(hostname3, domain2) {
8635
+ if (!AUTHOR_PLATFORMS.has(domain2)) return null;
8636
+ const author = hostname3.slice(0, -(domain2.length + 1)).split(".").at(-1);
8637
+ return author && !PLATFORM_LABELS.has(author) ? `${author}.${domain2}` : null;
8609
8638
  }
8610
8639
 
8611
8640
  // ../protocol/src/payee.ts
@@ -8646,6 +8675,26 @@ var X_RESERVED = /* @__PURE__ */ new Set([
8646
8675
  "tos"
8647
8676
  ]);
8648
8677
  var X_HANDLE = /^[a-z0-9_]{1,15}$/;
8678
+ var SHORTENER_HOSTS = /* @__PURE__ */ new Set([
8679
+ "bit.ly",
8680
+ "buff.ly",
8681
+ "cutt.ly",
8682
+ "dlvr.it",
8683
+ "goo.gl",
8684
+ "ift.tt",
8685
+ "is.gd",
8686
+ "lnkd.in",
8687
+ "ow.ly",
8688
+ "rb.gy",
8689
+ "rebrand.ly",
8690
+ "s.id",
8691
+ "shorturl.at",
8692
+ "t.co",
8693
+ "t.ly",
8694
+ "tiny.cc",
8695
+ "tinyurl.com",
8696
+ "trib.al"
8697
+ ]);
8649
8698
  function xPayee(handle) {
8650
8699
  const lower = handle.replace(/^@/, "").toLowerCase();
8651
8700
  return X_HANDLE.test(lower) && !X_RESERVED.has(lower) ? `${X_PAYEE_PREFIX}${lower}` : null;
@@ -8663,6 +8712,7 @@ function normalizePayee(input2) {
8663
8712
  return null;
8664
8713
  }
8665
8714
  const host = url2.hostname.toLowerCase().replace(/\.$/, "");
8715
+ if (SHORTENER_HOSTS.has(host.replace(/^www\./, ""))) return null;
8666
8716
  const bareHost = !/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) && !trimmed.includes("/");
8667
8717
  if (X_HOSTS.has(host) && !bareHost && (url2.protocol === "https:" || url2.protocol === "http:")) {
8668
8718
  const [first, second] = url2.pathname.split("/").filter(Boolean);
@@ -8695,6 +8745,180 @@ function fromBase64Url(value) {
8695
8745
  return bytes;
8696
8746
  }
8697
8747
 
8748
+ // ../protocol/src/exclusions.ts
8749
+ var DEFAULT_EXCLUSIONS = {
8750
+ email: {
8751
+ label: "Email",
8752
+ hosts: [
8753
+ "mail.google.com",
8754
+ "inbox.google.com",
8755
+ "outlook.live.com",
8756
+ "outlook.office.com",
8757
+ "outlook.office365.com",
8758
+ "mail.yahoo.com",
8759
+ "mail.aol.com",
8760
+ "mail.proton.me",
8761
+ "mail.zoho.com",
8762
+ "fastmail.com",
8763
+ "hey.com",
8764
+ "tuta.com",
8765
+ "gmx.com",
8766
+ "mail.com",
8767
+ "icloud.com"
8768
+ ]
8769
+ },
8770
+ workspace: {
8771
+ label: "Chat, calendars and documents",
8772
+ hosts: [
8773
+ "calendar.google.com",
8774
+ "drive.google.com",
8775
+ "docs.google.com",
8776
+ "messages.google.com",
8777
+ "chat.google.com",
8778
+ "app.slack.com",
8779
+ "teams.microsoft.com",
8780
+ "teams.live.com",
8781
+ "web.whatsapp.com",
8782
+ "web.telegram.org",
8783
+ "messenger.com",
8784
+ "discord.com",
8785
+ "notion.so",
8786
+ "dropbox.com",
8787
+ "onedrive.live.com"
8788
+ ]
8789
+ },
8790
+ auth: {
8791
+ label: "Sign-in and passwords",
8792
+ hosts: [
8793
+ "accounts.google.com",
8794
+ "myaccount.google.com",
8795
+ "login.microsoftonline.com",
8796
+ "login.live.com",
8797
+ "account.microsoft.com",
8798
+ "appleid.apple.com",
8799
+ "account.apple.com",
8800
+ "login.yahoo.com",
8801
+ "account.proton.me",
8802
+ "auth0.com",
8803
+ "okta.com",
8804
+ "onelogin.com",
8805
+ "duosecurity.com",
8806
+ "login.gov",
8807
+ "id.me",
8808
+ "1password.com",
8809
+ "bitwarden.com",
8810
+ "lastpass.com",
8811
+ "dashlane.com",
8812
+ "wallet.tempo.xyz"
8813
+ ]
8814
+ },
8815
+ finance: {
8816
+ label: "Banking and money",
8817
+ hosts: [
8818
+ "chase.com",
8819
+ "bankofamerica.com",
8820
+ "wellsfargo.com",
8821
+ "citi.com",
8822
+ "citibank.com",
8823
+ "capitalone.com",
8824
+ "usbank.com",
8825
+ "pnc.com",
8826
+ "tdbank.com",
8827
+ "ally.com",
8828
+ "discover.com",
8829
+ "americanexpress.com",
8830
+ "schwab.com",
8831
+ "fidelity.com",
8832
+ "vanguard.com",
8833
+ "etrade.com",
8834
+ "morganstanley.com",
8835
+ "marcus.com",
8836
+ "interactivebrokers.com",
8837
+ "robinhood.com",
8838
+ "sofi.com",
8839
+ "chime.com",
8840
+ "mercury.com",
8841
+ "brex.com",
8842
+ "paypal.com",
8843
+ "venmo.com",
8844
+ "wise.com",
8845
+ "revolut.com",
8846
+ "monzo.com",
8847
+ "n26.com",
8848
+ "hsbc.com",
8849
+ "hsbc.co.uk",
8850
+ "barclays.co.uk",
8851
+ "lloydsbank.com",
8852
+ "natwest.com",
8853
+ "santander.co.uk",
8854
+ "rbcroyalbank.com",
8855
+ "td.com",
8856
+ "scotiabank.com",
8857
+ "coinbase.com",
8858
+ "kraken.com",
8859
+ "binance.com",
8860
+ "creditkarma.com",
8861
+ "experian.com",
8862
+ "equifax.com",
8863
+ "transunion.com",
8864
+ "irs.gov",
8865
+ "intuit.com",
8866
+ "dashboard.stripe.com"
8867
+ ]
8868
+ },
8869
+ health: {
8870
+ label: "Health portals",
8871
+ hosts: [
8872
+ "mychart.com",
8873
+ "mychart.org",
8874
+ "kp.org",
8875
+ "healthcare.gov",
8876
+ "medicare.gov",
8877
+ "cvs.com",
8878
+ "walgreens.com",
8879
+ "goodrx.com",
8880
+ "zocdoc.com",
8881
+ "teladoc.com",
8882
+ "onemedical.com",
8883
+ "betterhelp.com",
8884
+ "talkspace.com",
8885
+ "labcorp.com",
8886
+ "questdiagnostics.com",
8887
+ "23andme.com",
8888
+ "aetna.com",
8889
+ "anthem.com",
8890
+ "cigna.com",
8891
+ "uhc.com",
8892
+ "humana.com",
8893
+ "bcbs.com",
8894
+ "hims.com",
8895
+ "forhers.com",
8896
+ "plannedparenthood.org"
8897
+ ]
8898
+ }
8899
+ };
8900
+ var EXCLUSION_CATEGORIES = Object.keys(DEFAULT_EXCLUSIONS);
8901
+ function matchesRule(host, domain2, rule) {
8902
+ const normalized = rule.toLowerCase().replace(/^\*\./, "").replace(/^www\./, "");
8903
+ return host === normalized || host.endsWith(`.${normalized}`) || domain2 === normalized;
8904
+ }
8905
+ function isExcludedByDefault(url2) {
8906
+ const domain2 = normalizePayee(url2);
8907
+ const host = hostOf(url2);
8908
+ if (!domain2 || !host) return true;
8909
+ return EXCLUSION_CATEGORIES.some(
8910
+ (category) => DEFAULT_EXCLUSIONS[category].hosts.some((rule) => matchesRule(host, domain2, rule))
8911
+ );
8912
+ }
8913
+ function hostOf(url2) {
8914
+ try {
8915
+ const parsed = new URL(/^[a-z][a-z0-9+.-]*:/i.test(url2) ? url2 : `https://${url2}`);
8916
+ return parsed.hostname.toLowerCase().replace(/\.$/, "") || null;
8917
+ } catch {
8918
+ return null;
8919
+ }
8920
+ }
8921
+
8698
8922
  // ../protocol/src/money.ts
8699
8923
  function formatUsd(units) {
8700
8924
  const negative = units < 0n;
@@ -35704,6 +35928,26 @@ function startRefill(script = process.argv[1]) {
35704
35928
  } catch {
35705
35929
  }
35706
35930
  }
35931
+ function startPayment(payload, script = process.argv[1]) {
35932
+ return new Promise((resolve) => {
35933
+ try {
35934
+ if (!script) return resolve(false);
35935
+ const child = spawn(process.execPath, [...process.execArgv, script, "pay"], {
35936
+ detached: true,
35937
+ env: process.env,
35938
+ stdio: ["pipe", "ignore", "ignore"]
35939
+ });
35940
+ child.on("error", () => resolve(false));
35941
+ child.stdin.on("error", () => resolve(false));
35942
+ child.stdin.end(payload, () => {
35943
+ child.unref();
35944
+ resolve(true);
35945
+ });
35946
+ } catch {
35947
+ resolve(false);
35948
+ }
35949
+ });
35950
+ }
35707
35951
 
35708
35952
  // src/codex.ts
35709
35953
  import { mkdir as mkdir2, readFile as readFile3 } from "node:fs/promises";
@@ -35929,9 +36173,10 @@ export {
35929
36173
  MIN_TOKENS_PER_PAGE,
35930
36174
  MAX_TOKENS_PER_PAGE,
35931
36175
  clampTokensPerPage,
36176
+ normalizePayee,
36177
+ isExcludedByDefault,
35932
36178
  formatUsd,
35933
36179
  tokensToUnits,
35934
- normalizePayee,
35935
36180
  LeaderboardResponse,
35936
36181
  PatronInfo,
35937
36182
  profileMessage,
@@ -35963,7 +36208,8 @@ export {
35963
36208
  resolveSettings,
35964
36209
  settingsFor,
35965
36210
  FileTokenStore,
35966
- startRefill
36211
+ startRefill,
36212
+ startPayment
35967
36213
  };
35968
36214
  /*! Bundled license information:
35969
36215
 
@@ -4,6 +4,7 @@ import {
4
4
  FileTokenStore,
5
5
  clampTokensPerPage,
6
6
  codexHome,
7
+ isExcludedByDefault,
7
8
  loadIssuer,
8
9
  needsRefill,
9
10
  normalizePayee,
@@ -11,8 +12,9 @@ import {
11
12
  resolveSettings,
12
13
  settingsFor,
13
14
  spend,
15
+ startPayment,
14
16
  startRefill
15
- } from "./chunk-4Z6ZIXMK.js";
17
+ } from "./chunk-GYL4GCNU.js";
16
18
  import {
17
19
  readConfig,
18
20
  readUsage,
@@ -35,6 +37,7 @@ function hookUrls(payload) {
35
37
  const tool = payload.tool_name;
36
38
  const response = "tool_response" in payload ? payload.tool_response : jsonValue(payload.tool_output);
37
39
  if (FETCH_TOOLS.has(tool)) {
40
+ if (!succeeded(response)) return [];
38
41
  const input = payload.tool_input;
39
42
  if (isRecord(input) && typeof input.url === "string") return [input.url];
40
43
  const urls2 = [];
@@ -50,12 +53,20 @@ function hookUrls(payload) {
50
53
  function hookDomains(payload) {
51
54
  const domains = [];
52
55
  for (const url of hookUrls(payload)) {
53
- const domain = normalizePayee(url);
56
+ const domain = payableDomain(url);
54
57
  if (domain && !domains.includes(domain)) domains.push(domain);
55
58
  if (domains.length >= MAX_DOMAINS_PER_HOOK) break;
56
59
  }
57
60
  return domains;
58
61
  }
62
+ function payableDomain(url) {
63
+ return isExcludedByDefault(url) ? null : normalizePayee(url);
64
+ }
65
+ function succeeded(response) {
66
+ if (!isRecord(response)) return true;
67
+ const status = response.code ?? response.status ?? response.statusCode;
68
+ return typeof status !== "number" || status >= 200 && status < 300;
69
+ }
59
70
  function turnEnd(payload) {
60
71
  if (!isRecord(payload)) return void 0;
61
72
  if (payload.hook_event_name === "Stop" && typeof payload.turn_id === "string" && typeof payload.transcript_path === "string")
@@ -179,7 +190,7 @@ async function takeTranscriptDomains(home, transcriptPath2, options) {
179
190
  const known = /* @__PURE__ */ new Set([...state.paid, ...state.pending]);
180
191
  const pending = [...state.pending];
181
192
  for (const url of codexRolloutUrls(lines)) {
182
- const domain = normalizePayee(url);
193
+ const domain = payableDomain(url);
183
194
  if (domain && !known.has(domain)) {
184
195
  known.add(domain);
185
196
  pending.push(domain);
@@ -289,6 +300,7 @@ function isRecord2(value) {
289
300
 
290
301
  // src/hook.ts
291
302
  var HOOK_TIMEOUT_MS = 2e3;
303
+ var PAY_TIMEOUT_MS = 2e4;
292
304
  var ABORT_MARGIN_MS = 500;
293
305
  async function runHook(dependencies, timeoutMs = HOOK_TIMEOUT_MS) {
294
306
  let timer;
@@ -337,25 +349,28 @@ async function payReads(dependencies, deadline) {
337
349
  let empty = false;
338
350
  const perPage = clampTokensPerPage(config.tokensPerPage);
339
351
  let pending = domains.slice();
340
- rounds: for (let round = 0; round < perPage && pending.length > 0; round++) {
352
+ for (let round = 0; round < perPage && pending.length > 0; round++) {
353
+ if (Date.now() >= deadline) break;
354
+ const results = await Promise.all(
355
+ pending.map(
356
+ (domain) => spend({
357
+ domain,
358
+ fetch: boundedFetch,
359
+ issuer,
360
+ relayUrl: settings.relayUrl,
361
+ store
362
+ }).catch(() => null)
363
+ )
364
+ );
341
365
  const next = [];
342
- for (const domain of pending) {
343
- if (Date.now() >= deadline) break rounds;
344
- const result = await spend({
345
- domain,
346
- fetch: boundedFetch,
347
- issuer,
348
- relayUrl: settings.relayUrl,
349
- store
350
- }).catch(() => null);
351
- if (result === void 0) {
352
- empty = true;
353
- break rounds;
354
- }
355
- if (!result?.ok) continue;
366
+ results.forEach((result, index) => {
367
+ const domain = pending[index];
368
+ if (result === void 0) empty = true;
369
+ if (!result?.ok) return;
356
370
  if (round === 0) paid.push(result.domain || domain);
357
371
  next.push(domain);
358
- }
372
+ });
373
+ if (empty) break;
359
374
  pending = next;
360
375
  }
361
376
  const now = dependencies.now?.() ?? /* @__PURE__ */ new Date();
@@ -393,13 +408,28 @@ var MAX_INPUT_BYTES = 8 * 1024 * 1024;
393
408
  async function runHookProcess(argument = process.argv[3]) {
394
409
  setTimeout(() => process.exit(0), HOOK_TIMEOUT_MS + 500).unref();
395
410
  const raw = argument ?? await readStdin(HOOK_TIMEOUT_MS / 2);
396
- if (isCursorPayload(parseHookInput(raw))) process.stdout.write("{}\n");
411
+ const payload = parseHookInput(raw);
412
+ if (isCursorPayload(payload)) process.stdout.write("{}\n");
413
+ if (!turnEnd(payload) && hookDomains(payload).length === 0) return;
414
+ if (await startPayment(raw)) return;
397
415
  await runHook({
398
416
  readInput: async () => raw,
399
417
  settings: resolveSettings(),
400
418
  startRefill: () => startRefill()
401
419
  });
402
420
  }
421
+ async function runPayProcess() {
422
+ setTimeout(() => process.exit(0), PAY_TIMEOUT_MS + 1e3).unref();
423
+ const raw = await readStdin(HOOK_TIMEOUT_MS);
424
+ await runHook(
425
+ {
426
+ readInput: async () => raw,
427
+ settings: resolveSettings(),
428
+ startRefill: () => startRefill()
429
+ },
430
+ PAY_TIMEOUT_MS
431
+ );
432
+ }
403
433
  function readStdin(timeoutMs) {
404
434
  return new Promise((resolve) => {
405
435
  const chunks = [];
@@ -425,5 +455,6 @@ function readStdin(timeoutMs) {
425
455
  });
426
456
  }
427
457
  export {
428
- runHookProcess
458
+ runHookProcess,
459
+ runPayProcess
429
460
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tempoxyz/patron",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "Pay the websites your AI agents read, privately",
5
5
  "type": "module",
6
6
  "bin": {