@true402.dev/openclaw-swap-guard 1.0.0 → 1.0.2

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
@@ -19,7 +19,7 @@ Before a tool call that buys a token, the guard asks [true402](https://true402.d
19
19
 
20
20
  - **Blocks** only on a definite danger verdict: an EVM token rated *avoid* (cannot be sold, extreme sell tax, liquidity just pulled…), or a Solana mint that can never be sold, can be seized, or is critical risk. The agent sees the reasons and is told not to retry with another tool.
21
21
  - **Lets the trade through** when the token checks out — and, by default, when it could not be checked at all (true402 unreachable, free checks used up, the address is a wallet rather than a token). A guard that breaks your trading whenever a third party hiccups is a guard you uninstall. Change that with `onUnchecked`.
22
- - Checks only the token being **bought**. The token you are selling is never checked: blocking the exit from a token whose liquidity is being pulled would trap you in it.
22
+ - Never blocks a **sale**. The token you are selling is not checked, and when a command is ambiguous enough that a dangerous token *might* be the one being sold, the guard asks you instead of blocking — trapping you in a token whose liquidity is being pulled would be worse than the trade.
23
23
  - Skips majors and stablecoins (WETH, USDC, USDT, DAI, WBNB, SOL…), reuses a verdict for 10 minutes, and checks up to 5 distinct tokens per call — a call naming more is held for your approval (or blocked with `onUnchecked: block`). A token on a chain true402 does not serve counts as "could not be checked".
24
24
 
25
25
  It recognises buys in shell commands the way trading skills run them (`--token-out 0x…`, `trade <amount> <from> <to>`, `<script> <CA> --chain base`, `--chain solana <mint>`) and in structured tool parameters (`tokenOut`, `outputMint`, `token`, …). Command parsing fails closed: a token is checked on the default chain and on every chain the command names, and anything ambiguous — a `#`, `$'…'`, unbalanced quotes — means every address is checked, including the one being sold. A plain-English order that names no address (`"buy $20 of PEPE"`), or a token passed through a shell variable, cannot be checked.
@@ -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 a day** (`maxPaidChecksPerDay`) — so never more than $1.00 a day by default. Past the daily limit, only free checks run, and the guard logs a warning.
38
40
 
39
41
  ## Settings
40
42
 
@@ -46,8 +48,9 @@ All optional, under the plugin's config (`true402-swap-guard`):
46
48
  | `onCaution` | `allow` | `ask` = request your approval on a *caution* verdict |
47
49
  | `onUnchecked` | `allow` | `ask` or `block` when a token could not be checked |
48
50
  | `allowTokens` | `[]` | Token addresses never checked |
49
- | `timeoutMs` | `15000` | Give up on one check after this long (then it is "unchecked") |
50
- | `maxAmountUsd` | `0.25` | Never sign a single check payment above this |
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 |
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
 
package/dist/index.js CHANGED
@@ -101,6 +101,11 @@ function fromCommand(cmd, defaultChain) {
101
101
  const evmChains = [.../* @__PURE__ */ new Set([...[...supported].filter((c) => c !== "solana"), defaultChain])];
102
102
  const unambiguous = !/#|\$'/.test(cmd) && (cmd.match(/"/g) ?? []).length % 2 === 0 && (cmd.match(/'/g) ?? []).length % 2 === 0;
103
103
  const sold = /* @__PURE__ */ new Map();
104
+ const soldAny = new Set(flags(cmd, IN_FLAGS).map((v) => v.toLowerCase()));
105
+ for (const seg of cmd.split(/;|&&?|\|\|?|\n/)) {
106
+ const p = /\bawal\b/.test(seg) ? seg.match(/\btrade\s+(\S+)\s+(\S+)\s+(\S+)/) : null;
107
+ if (p) soldAny.add(p[2].replace(/['"]/g, "").toLowerCase());
108
+ }
104
109
  if (unambiguous) {
105
110
  const markSold = (v) => sold.set(v.toLowerCase(), (sold.get(v.toLowerCase()) ?? 0) + 1);
106
111
  for (const v of flags(cmd, IN_FLAGS)) markSold(v);
@@ -112,14 +117,17 @@ function fromCommand(cmd, defaultChain) {
112
117
  const mentions = (v) => cmd.toLowerCase().split(v.toLowerCase()).length - 1;
113
118
  const keep = (v) => !skip(v) && (sold.get(v.toLowerCase()) ?? 0) < mentions(v);
114
119
  const out = [];
120
+ const boughtByFlag = new Set(flags(cmd, OUT_FLAGS).map((v) => v.toLowerCase()));
121
+ const maybeSold = (tok) => !unambiguous && soldAny.has(tok.toLowerCase()) && !boughtByFlag.has(tok.toLowerCase());
122
+ const push = (t) => out.push(maybeSold(t.token) ? { ...t, maybeSold: true } : t);
115
123
  const evm = [...new Set([...cmd.matchAll(EVM_ADDRESS)].map((m) => m[0]))].filter(keep);
116
- for (const chain of evmChains) for (const a of evm) out.push({ chain, token: a });
117
- if (unsupportedNamed) for (const a of evm) out.push({ chain: "unsupported", token: a });
124
+ for (const chain of evmChains) for (const a of evm) push({ chain, token: a });
125
+ if (unsupportedNamed) for (const a of evm) push({ chain: "unsupported", token: a });
118
126
  const words = [...new Set(cmd.split(/[\s;|&`()]+/).map((x) => x.replace(/^[^=]*=/, "").replace(/['"]/g, "")))];
119
127
  if (supported.has("solana")) {
120
- for (const w of words) if (SOLANA_MINT.test(w) && keep(w)) out.push({ chain: "solana", token: w });
128
+ for (const w of words) if (SOLANA_MINT.test(w) && keep(w)) push({ chain: "solana", token: w });
121
129
  } else if (!supported.size) {
122
- for (const v of flags(cmd, OUT_FLAGS)) if (SOLANA_MINT.test(v) && !isEvm(v) && keep(v)) out.push({ chain: "solana", token: v });
130
+ for (const v of flags(cmd, OUT_FLAGS)) if (SOLANA_MINT.test(v) && !isEvm(v) && keep(v)) push({ chain: "solana", token: v });
123
131
  }
124
132
  return out;
125
133
  }
@@ -220,29 +228,33 @@ function createGuard(check, cfg = {}, now = Date.now) {
220
228
  return { t: ts[0], v: { level: "unchecked", reasons: [...new Set(vs.flatMap((x) => x.reasons))] } };
221
229
  })
222
230
  );
223
- const danger = results.find((r) => r.v.level === "danger");
231
+ const danger = results.find((r) => r.v.level === "danger" && !r.t.maybeSold);
224
232
  if (danger) {
225
233
  return {
226
234
  block: true,
227
235
  blockReason: `true402 swap guard blocked this trade: ${danger.t.token} on ${danger.t.chain} was rated dangerous${reasonText(danger.v)}. Do not retry it with another tool. To trade it anyway, add the address to the plugin's allowTokens setting.`
228
236
  };
229
237
  }
238
+ const unchecked = results.find((r) => r.v.level === "unchecked");
239
+ if (unchecked) warnOnce(unchecked.v.reasons[0] ?? "unknown");
240
+ const uncheckedWhy = unchecked && `true402 could not check ${unchecked.t.token} on ${unchecked.t.chain}${reasonText(unchecked.v)}.`;
241
+ if (unchecked && cfg.onUnchecked === "block") return { block: true, blockReason: `true402 swap guard blocked this trade: ${uncheckedWhy}` };
242
+ const asks = [];
243
+ let severity = "info";
244
+ const dangerSold = results.find((r) => r.v.level === "danger" && r.t.maybeSold);
245
+ if (dangerSold) {
246
+ asks.push(`true402 rated ${dangerSold.t.token} on ${dangerSold.t.chain} dangerous${reasonText(dangerSold.v)}. If you are selling it, approve; if this buys it, deny.`);
247
+ severity = "critical";
248
+ }
230
249
  const caution = results.find((r) => r.v.level === "caution");
231
250
  if (caution && cfg.onCaution === "ask") {
232
- return {
233
- requireApproval: {
234
- title: "Risky token \u2014 confirm the trade",
235
- description: `true402 rated ${caution.t.token} on ${caution.t.chain} "caution"${reasonText(caution.v)}.`,
236
- severity: "warning"
237
- }
238
- };
251
+ asks.push(`true402 rated ${caution.t.token} on ${caution.t.chain} "caution"${reasonText(caution.v)}.`);
252
+ if (severity !== "critical") severity = "warning";
239
253
  }
240
- const unchecked = results.find((r) => r.v.level === "unchecked");
241
- if (unchecked) warnOnce(unchecked.v.reasons[0] ?? "unknown");
242
- if (unchecked && cfg.onUnchecked && cfg.onUnchecked !== "allow") {
243
- const why = `true402 could not check ${unchecked.t.token} on ${unchecked.t.chain}${reasonText(unchecked.v)}.`;
244
- if (cfg.onUnchecked === "block") return { block: true, blockReason: `true402 swap guard blocked this trade: ${why}` };
245
- return { requireApproval: { title: "Unchecked token \u2014 confirm the trade", description: why, severity: "info" } };
254
+ if (unchecked && cfg.onUnchecked === "ask") asks.push(uncheckedWhy);
255
+ if (asks.length) {
256
+ const title = dangerSold ? "Dangerous token \u2014 confirm the trade" : caution && cfg.onCaution === "ask" ? "Risky token \u2014 confirm the trade" : "Unchecked token \u2014 confirm the trade";
257
+ return { requireApproval: { title, description: asks.join(" "), severity } };
246
258
  }
247
259
  return void 0;
248
260
  };
@@ -262,7 +274,7 @@ import { privateKeyToAccount } from "viem/accounts";
262
274
  import { base } from "viem/chains";
263
275
 
264
276
  // src/client.ts
265
- var CLIENT = { id: "openclaw-guard", version: "1.0.0", package: "@true402.dev/openclaw-swap-guard", install: "openclaw plugins install @true402.dev/openclaw-swap-guard" };
277
+ var CLIENT = { id: "openclaw-guard", version: "1.0.2", package: "@true402.dev/openclaw-swap-guard", install: "openclaw plugins install @true402.dev/openclaw-swap-guard" };
266
278
 
267
279
  // src/x402.ts
268
280
  var USDC_ABI = [
@@ -435,6 +447,8 @@ async function payStall(path, payload, opts) {
435
447
  }
436
448
 
437
449
  // src/check.ts
450
+ var DEFAULT_MAX_PER_CHECK_USD = 0.02;
451
+ var DEFAULT_MAX_PAID_CHECKS_PER_DAY = 50;
438
452
  var NOT_A_TOKEN = /* @__PURE__ */ new Set(["not_a_contract", "mint_unreadable", "mint_not_initialized"]);
439
453
  var SOL_NEVER_SELLABLE = /* @__PURE__ */ new Set(["non_transferable", "permanent_delegate"]);
440
454
  var SOL_SELL_BLOCKABLE = /* @__PURE__ */ new Set(["freeze_authority_active", "pausable", "transfer_hook", "default_frozen"]);
@@ -478,10 +492,12 @@ function strings(v) {
478
492
  function createChecker(cfg = {}) {
479
493
  const baseUrl = cfg.baseUrl ?? "https://true402.dev/api";
480
494
  const timeoutMs = cfg.timeoutMs ?? 15e3;
481
- async function ask(path, token) {
482
- if (cfg.payerPrivateKey) {
483
- return payStall(path, { token }, { payerPrivateKey: cfg.payerPrivateKey, baseUrl, maxAmountUsd: cfg.maxAmountUsd });
484
- }
495
+ const perCheck = cfg.maxAmountUsd ?? DEFAULT_MAX_PER_CHECK_USD;
496
+ const perDay = cfg.maxPaidChecksPerDay ?? DEFAULT_MAX_PAID_CHECKS_PER_DAY;
497
+ const now = cfg.now ?? Date.now;
498
+ let day = "";
499
+ let paidToday = 0;
500
+ async function free(path, token) {
485
501
  const res = await fetch(`${baseUrl}${path}`, {
486
502
  method: "POST",
487
503
  headers: { "content-type": "application/json", "user-agent": `true402-${CLIENT.id}/${CLIENT.version} (+https://true402.dev)` },
@@ -489,22 +505,21 @@ function createChecker(cfg = {}) {
489
505
  redirect: "manual",
490
506
  signal: AbortSignal.timeout(timeoutMs)
491
507
  });
492
- if (res.status === 402) throw new Error("the free daily checks are used up \u2014 set PAYER_PRIVATE_KEY to pay per check");
508
+ if (res.status === 402) return null;
493
509
  if (!res.ok) throw new Error(`true402 answered HTTP ${res.status}`);
494
510
  return res.json();
495
511
  }
496
512
  return async (t) => {
497
513
  const path = t.chain === "solana" ? "/v1/solana/token-safety" : `/v1/${t.chain}/token-report`;
498
- let timer;
499
- const deadline = new Promise((_, reject) => {
500
- timer = setTimeout(() => reject(new Error(`no answer within ${timeoutMs} ms`)), timeoutMs);
501
- });
502
- try {
503
- const body = await Promise.race([ask(path, t.token), deadline]);
504
- return t.chain === "solana" ? solanaVerdict(body) : evmVerdict(body, t.chain);
505
- } finally {
506
- clearTimeout(timer);
507
- }
514
+ const toVerdict = (body2) => t.chain === "solana" ? solanaVerdict(body2) : evmVerdict(body2, t.chain);
515
+ const body = await free(path, t.token);
516
+ if (body !== null) return toVerdict(body);
517
+ if (!cfg.payerPrivateKey) throw new Error("the free daily checks are used up \u2014 set TRUE402_PAYER_PRIVATE_KEY to pay per check");
518
+ const today = new Date(now()).toISOString().slice(0, 10);
519
+ if (today !== day) [day, paidToday] = [today, 0];
520
+ if (paidToday >= perDay) throw new Error(`the daily limit of ${perDay} paid checks is reached (maxPaidChecksPerDay)`);
521
+ paidToday++;
522
+ return toVerdict(await payStall(path, { token: t.token }, { payerPrivateKey: cfg.payerPrivateKey, baseUrl, maxAmountUsd: perCheck }));
508
523
  };
509
524
  }
510
525
 
@@ -520,7 +535,8 @@ function readConfig(raw) {
520
535
  onUnchecked: pick(c.onUnchecked, ["allow", "ask", "block"]),
521
536
  allowTokens: Array.isArray(c.allowTokens) ? c.allowTokens.filter((x) => typeof x === "string") : void 0,
522
537
  timeoutMs: num(c.timeoutMs),
523
- maxAmountUsd: num(c.maxAmountUsd)
538
+ maxAmountUsd: num(c.maxAmountUsd),
539
+ maxPaidChecksPerDay: num(c.maxPaidChecksPerDay)
524
540
  };
525
541
  }
526
542
  var index_default = definePluginEntry({
@@ -529,7 +545,7 @@ var index_default = definePluginEntry({
529
545
  description: "Blocks token buys when true402 finds a honeypot or a rug in progress (Base, Ethereum, BSC, Solana).",
530
546
  register(api) {
531
547
  const cfg = readConfig(api.pluginConfig);
532
- const payerPrivateKey = process.env.TRUE402_PAYER_PRIVATE_KEY || process.env.PAYER_PRIVATE_KEY || void 0;
548
+ const payerPrivateKey = process.env.TRUE402_PAYER_PRIVATE_KEY || void 0;
533
549
  const warn = (m) => api.logger?.warn ? api.logger.warn(m) : console.warn(m);
534
550
  const guard = createGuard(createChecker({ ...cfg, payerPrivateKey }), { ...cfg, warn });
535
551
  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.0",
3
+ "version": "1.0.2",
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",
@@ -29,7 +29,7 @@
29
29
  "homepage": "https://true402.dev",
30
30
  "repository": {
31
31
  "type": "git",
32
- "url": "https://github.com/true402/openclaw-swap-guard"
32
+ "url": "git+https://github.com/true402/openclaw-swap-guard.git"
33
33
  },
34
34
  "license": "MIT",
35
35
  "dependencies": {