@true402.dev/openclaw-swap-guard 1.0.1 → 1.0.3

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
@@ -28,13 +28,15 @@ It recognises buys in shell commands the way trading skills run them (`--token-o
28
28
 
29
29
  No setup is needed for a few checks a day: the guard uses true402's free daily allowance.
30
30
 
31
- For more, give it a **separate, low-balance** Base wallet holding a little USDC (gas is sponsored):
31
+ For more, give it a **dedicated, low-balance** Base wallet holding a little USDC (gas is sponsored):
32
32
 
33
33
  ```bash
34
- export TRUE402_PAYER_PRIVATE_KEY=0x… # or PAYER_PRIVATE_KEY
34
+ export TRUE402_PAYER_PRIVATE_KEY=0x…
35
35
  ```
36
36
 
37
- Each check then pays the exact quoted price over [x402](https://x402.org) — no account, no API key. The key is read from the environment, never from OpenClaw's config file, and is never sent anywhere: it signs a USDC authorization locally. The guard **refuses to sign more than $0.25** for a single check.
37
+ Only this dedicated variable is read — never a generic `PAYER_PRIVATE_KEY` another tool may have set. Each check then pays the exact quoted price over [x402](https://x402.org), no account and no API key. The key stays on your machine (it authorizes each USDC payment locally) and is never written to OpenClaw's config.
38
+
39
+ Spending is bounded twice: **at most $0.02 per check** (`maxAmountUsd`) and **at most 50 paid checks per UTC day** (`maxPaidChecksPerDay`) — at most $1.00 a day by default. The day's count is kept in `$OPENCLAW_STATE_DIR/true402-swap-guard/spend.json` (default `~/.openclaw/…`), so restarting OpenClaw does not reset it; if that file cannot be written, no payment is made. Past the limit only free checks run, and the guard logs a warning. The limit is per machine: several OpenClaw installs with their own state directories each have their own.
38
40
 
39
41
  ## Settings
40
42
 
@@ -47,11 +49,14 @@ All optional, under the plugin's config (`true402-swap-guard`):
47
49
  | `onUnchecked` | `allow` | `ask` or `block` when a token could not be checked |
48
50
  | `allowTokens` | `[]` | Token addresses never checked |
49
51
  | `timeoutMs` | `15000` | Give up on a free check after this long (then it is "unchecked"). A paid check is never abandoned once paid — you get the answer you paid for |
50
- | `maxAmountUsd` | `0.25` | Never sign a single check payment above this |
52
+ | `maxAmountUsd` | `0.02` | Never pay more than this for one check |
53
+ | `maxPaidChecksPerDay` | `50` | Paid checks per UTC day; after that only free checks run |
51
54
 
52
55
  ## What is sent
53
56
 
54
- Only the token address and its chain, to `true402.dev`. Nothing about your wallet, your prompts or the trade itself.
57
+ A **free check** sends only the token address and its chain to `true402.dev` — nothing about your wallet, your prompts or the trade.
58
+
59
+ A **paid check** also sends a signed USDC payment authorization. That necessarily reveals the paying wallet's **address** to true402, and the payment is settled on Base, where it is public. That is why the wallet should be a dedicated, low-balance one. The private key itself never leaves your machine.
55
60
 
56
61
  ## License
57
62
 
package/dist/index.js CHANGED
@@ -267,6 +267,11 @@ function clean(s, max = 160) {
267
267
  return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
268
268
  }
269
269
 
270
+ // src/check.ts
271
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
272
+ import { homedir } from "os";
273
+ import { dirname, join } from "path";
274
+
270
275
  // src/x402.ts
271
276
  import { randomBytes } from "crypto";
272
277
  import { createPublicClient, http, getAddress } from "viem";
@@ -274,7 +279,7 @@ import { privateKeyToAccount } from "viem/accounts";
274
279
  import { base } from "viem/chains";
275
280
 
276
281
  // src/client.ts
277
- var CLIENT = { id: "openclaw-guard", version: "1.0.1", package: "@true402.dev/openclaw-swap-guard", install: "openclaw plugins install @true402.dev/openclaw-swap-guard" };
282
+ var CLIENT = { id: "openclaw-guard", version: "1.0.3", package: "@true402.dev/openclaw-swap-guard", install: "openclaw plugins install @true402.dev/openclaw-swap-guard" };
278
283
 
279
284
  // src/x402.ts
280
285
  var USDC_ABI = [
@@ -447,6 +452,24 @@ async function payStall(path, payload, opts) {
447
452
  }
448
453
 
449
454
  // src/check.ts
455
+ function fileSpendStore(path = join(process.env.OPENCLAW_STATE_DIR || join(homedir(), ".openclaw"), "true402-swap-guard", "spend.json")) {
456
+ return {
457
+ read() {
458
+ try {
459
+ const v = JSON.parse(readFileSync(path, "utf8"));
460
+ return { day: typeof v.day === "string" ? v.day : "", count: typeof v.count === "number" && v.count >= 0 ? v.count : 0 };
461
+ } catch {
462
+ return { day: "", count: 0 };
463
+ }
464
+ },
465
+ write(v) {
466
+ mkdirSync(dirname(path), { recursive: true });
467
+ writeFileSync(path, JSON.stringify(v), { mode: 384 });
468
+ }
469
+ };
470
+ }
471
+ var DEFAULT_MAX_PER_CHECK_USD = 0.02;
472
+ var DEFAULT_MAX_PAID_CHECKS_PER_DAY = 50;
450
473
  var NOT_A_TOKEN = /* @__PURE__ */ new Set(["not_a_contract", "mint_unreadable", "mint_not_initialized"]);
451
474
  var SOL_NEVER_SELLABLE = /* @__PURE__ */ new Set(["non_transferable", "permanent_delegate"]);
452
475
  var SOL_SELL_BLOCKABLE = /* @__PURE__ */ new Set(["freeze_authority_active", "pausable", "transfer_hook", "default_frozen"]);
@@ -490,6 +513,10 @@ function strings(v) {
490
513
  function createChecker(cfg = {}) {
491
514
  const baseUrl = cfg.baseUrl ?? "https://true402.dev/api";
492
515
  const timeoutMs = cfg.timeoutMs ?? 15e3;
516
+ const perCheck = cfg.maxAmountUsd ?? DEFAULT_MAX_PER_CHECK_USD;
517
+ const perDay = cfg.maxPaidChecksPerDay ?? DEFAULT_MAX_PAID_CHECKS_PER_DAY;
518
+ const now = cfg.now ?? Date.now;
519
+ const store = cfg.spendStore ?? fileSpendStore();
493
520
  async function free(path, token) {
494
521
  const res = await fetch(`${baseUrl}${path}`, {
495
522
  method: "POST",
@@ -507,8 +534,17 @@ function createChecker(cfg = {}) {
507
534
  const toVerdict = (body2) => t.chain === "solana" ? solanaVerdict(body2) : evmVerdict(body2, t.chain);
508
535
  const body = await free(path, t.token);
509
536
  if (body !== null) return toVerdict(body);
510
- if (!cfg.payerPrivateKey) throw new Error("the free daily checks are used up \u2014 set PAYER_PRIVATE_KEY to pay per check");
511
- return toVerdict(await payStall(path, { token: t.token }, { payerPrivateKey: cfg.payerPrivateKey, baseUrl, maxAmountUsd: cfg.maxAmountUsd }));
537
+ if (!cfg.payerPrivateKey) throw new Error("the free daily checks are used up \u2014 set TRUE402_PAYER_PRIVATE_KEY to pay per check");
538
+ const today = new Date(now()).toISOString().slice(0, 10);
539
+ const saved = store.read();
540
+ const paidToday = saved.day === today ? saved.count : 0;
541
+ if (paidToday >= perDay) throw new Error(`the daily limit of ${perDay} paid checks is reached (maxPaidChecksPerDay)`);
542
+ try {
543
+ store.write({ day: today, count: paidToday + 1 });
544
+ } catch {
545
+ throw new Error("could not record the paid-check count, so no payment was made");
546
+ }
547
+ return toVerdict(await payStall(path, { token: t.token }, { payerPrivateKey: cfg.payerPrivateKey, baseUrl, maxAmountUsd: perCheck }));
512
548
  };
513
549
  }
514
550
 
@@ -524,7 +560,8 @@ function readConfig(raw) {
524
560
  onUnchecked: pick(c.onUnchecked, ["allow", "ask", "block"]),
525
561
  allowTokens: Array.isArray(c.allowTokens) ? c.allowTokens.filter((x) => typeof x === "string") : void 0,
526
562
  timeoutMs: num(c.timeoutMs),
527
- maxAmountUsd: num(c.maxAmountUsd)
563
+ maxAmountUsd: num(c.maxAmountUsd),
564
+ maxPaidChecksPerDay: num(c.maxPaidChecksPerDay)
528
565
  };
529
566
  }
530
567
  var index_default = definePluginEntry({
@@ -533,7 +570,7 @@ var index_default = definePluginEntry({
533
570
  description: "Blocks token buys when true402 finds a honeypot or a rug in progress (Base, Ethereum, BSC, Solana).",
534
571
  register(api) {
535
572
  const cfg = readConfig(api.pluginConfig);
536
- const payerPrivateKey = process.env.TRUE402_PAYER_PRIVATE_KEY || process.env.PAYER_PRIVATE_KEY || void 0;
573
+ const payerPrivateKey = process.env.TRUE402_PAYER_PRIVATE_KEY || void 0;
537
574
  const warn = (m) => api.logger?.warn ? api.logger.warn(m) : console.warn(m);
538
575
  const guard = createGuard(createChecker({ ...cfg, payerPrivateKey }), { ...cfg, warn });
539
576
  api.on("before_tool_call", (event, ctx) => guard(event.toolName, event.params ?? {}, ctx?.abortSignal), { priority: 60 });
@@ -15,7 +15,8 @@
15
15
  "onUnchecked": { "type": "string", "enum": ["allow", "ask", "block"], "description": "When a token could not be checked (API unreachable, free checks used up): allow (default), ask, or block." },
16
16
  "allowTokens": { "type": "array", "items": { "type": "string" }, "description": "Token addresses never checked." },
17
17
  "timeoutMs": { "type": "number", "description": "Give up on one check after this many ms (default 15000)." },
18
- "maxAmountUsd": { "type": "number", "description": "Never sign a single check payment above this, in USDC (default 0.25)." }
18
+ "maxAmountUsd": { "type": "number", "description": "Never pay more than this for one check, in USDC (default 0.02)." },
19
+ "maxPaidChecksPerDay": { "type": "number", "description": "At most this many paid checks per day; beyond it only free checks run (default 50)." }
19
20
  }
20
21
  }
21
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@true402.dev/openclaw-swap-guard",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "OpenClaw plugin: before any tool call that buys a token, check it with true402 (executed buy/sell honeypot simulation) and block honeypots and rugs. Base, Ethereum, BSC, Solana. Free daily checks; pay-per-check over x402.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",