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

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +38 -34
  3. package/package.json +2 -2
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.
@@ -46,7 +46,7 @@ All optional, under the plugin's config (`true402-swap-guard`):
46
46
  | `onCaution` | `allow` | `ask` = request your approval on a *caution* verdict |
47
47
  | `onUnchecked` | `allow` | `ask` or `block` when a token could not be checked |
48
48
  | `allowTokens` | `[]` | Token addresses never checked |
49
- | `timeoutMs` | `15000` | Give up on one check after this long (then it is "unchecked") |
49
+ | `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
50
  | `maxAmountUsd` | `0.25` | Never sign a single check payment above this |
51
51
 
52
52
  ## What is sent
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.1", 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 = [
@@ -478,10 +490,7 @@ function strings(v) {
478
490
  function createChecker(cfg = {}) {
479
491
  const baseUrl = cfg.baseUrl ?? "https://true402.dev/api";
480
492
  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
- }
493
+ async function free(path, token) {
485
494
  const res = await fetch(`${baseUrl}${path}`, {
486
495
  method: "POST",
487
496
  headers: { "content-type": "application/json", "user-agent": `true402-${CLIENT.id}/${CLIENT.version} (+https://true402.dev)` },
@@ -489,22 +498,17 @@ function createChecker(cfg = {}) {
489
498
  redirect: "manual",
490
499
  signal: AbortSignal.timeout(timeoutMs)
491
500
  });
492
- if (res.status === 402) throw new Error("the free daily checks are used up \u2014 set PAYER_PRIVATE_KEY to pay per check");
501
+ if (res.status === 402) return null;
493
502
  if (!res.ok) throw new Error(`true402 answered HTTP ${res.status}`);
494
503
  return res.json();
495
504
  }
496
505
  return async (t) => {
497
506
  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
- }
507
+ const toVerdict = (body2) => t.chain === "solana" ? solanaVerdict(body2) : evmVerdict(body2, t.chain);
508
+ const body = await free(path, t.token);
509
+ 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 }));
508
512
  };
509
513
  }
510
514
 
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.1",
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": {