@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 +9 -6
- package/dist/index.js +52 -36
- package/openclaw.plugin.json +2 -1
- 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
|
-
-
|
|
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 **
|
|
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…
|
|
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)
|
|
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
|
|
50
|
-
| `maxAmountUsd` | `0.
|
|
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)
|
|
117
|
-
if (unsupportedNamed) for (const a of evm)
|
|
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))
|
|
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))
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
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.
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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)
|
|
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
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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 ||
|
|
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 });
|
package/openclaw.plugin.json
CHANGED
|
@@ -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
|
|
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.
|
|
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": {
|