@true402.dev/openclaw-swap-guard 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +57 -2
- package/dist/index.js +541 -0
- package/openclaw.plugin.json +21 -0
- package/package.json +71 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 true402
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,58 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @true402.dev/openclaw-swap-guard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**An OpenClaw plugin that checks every token your agent is about to buy, and blocks the trade when the token is a honeypot or a rug in progress.**
|
|
4
|
+
|
|
5
|
+
It hooks OpenClaw's `before_tool_call`, so it sits in front of *every* trading skill and MCP tool you have installed — Base trade skills, Uniswap, Jupiter, sniper bots, your own scripts. A skill the agent forgets to follow cannot skip it.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install @true402.dev/openclaw-swap-guard
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
Before a tool call that buys a token, the guard asks [true402](https://true402.dev):
|
|
14
|
+
|
|
15
|
+
| Chain | Check | Price after the free daily checks |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Base, Ethereum, BSC | `/v1/{chain}/token-report`: an executed buy **and sell** simulation, deployer history, recent liquidity pulls | $0.008 |
|
|
18
|
+
| Solana | `/v1/solana/token-safety`: freeze / mint authority, Token-2022 extensions, liquidity | $0.005 |
|
|
19
|
+
|
|
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
|
+
- **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.
|
|
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
|
+
|
|
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.
|
|
26
|
+
|
|
27
|
+
## Paying for checks
|
|
28
|
+
|
|
29
|
+
No setup is needed for a few checks a day: the guard uses true402's free daily allowance.
|
|
30
|
+
|
|
31
|
+
For more, give it a **separate, low-balance** Base wallet holding a little USDC (gas is sponsored):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
export TRUE402_PAYER_PRIVATE_KEY=0x… # or PAYER_PRIVATE_KEY
|
|
35
|
+
```
|
|
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.
|
|
38
|
+
|
|
39
|
+
## Settings
|
|
40
|
+
|
|
41
|
+
All optional, under the plugin's config (`true402-swap-guard`):
|
|
42
|
+
|
|
43
|
+
| Setting | Default | Meaning |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `defaultChain` | `base` | Chain assumed for a `0x` address when the trade names none (`base`, `ethereum`, `bsc`) |
|
|
46
|
+
| `onCaution` | `allow` | `ask` = request your approval on a *caution* verdict |
|
|
47
|
+
| `onUnchecked` | `allow` | `ask` or `block` when a token could not be checked |
|
|
48
|
+
| `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
|
+
|
|
52
|
+
## What is sent
|
|
53
|
+
|
|
54
|
+
Only the token address and its chain, to `true402.dev`. Nothing about your wallet, your prompts or the trade itself.
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
+
|
|
4
|
+
// src/detect.ts
|
|
5
|
+
var EVM_ADDRESS = /0x[a-fA-F0-9]{40}\b/g;
|
|
6
|
+
var SOLANA_MINT = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
7
|
+
var TRADE_VERB = /\b(swap|swaps|buy|buys|trade|snipe|ape|purchase|enter|market-buy|limit-buy)\b|tradr-enter|--token-out|--output-mint|--buy-token/i;
|
|
8
|
+
var TRADE_TOOL = /(swap|buy|trade|snipe|purchase|order)/i;
|
|
9
|
+
var OUT_KEYS = ["tokenout", "token_out", "outputmint", "output_mint", "outputtoken", "output_token", "totoken", "to_token", "buytoken", "buy_token", "tokentobuy", "mintout", "outmint", "to"];
|
|
10
|
+
var TOKEN_KEYS = ["token", "tokenaddress", "token_address", "mint", "ca", "contract", "contractaddress", "contract_address", "address", "asset"];
|
|
11
|
+
var PARTY_KEYS = /recipient|receiver|wallet|owner|sender|spender|from_address|payer|beneficiary|account|user/i;
|
|
12
|
+
var SKIP = /* @__PURE__ */ new Set(
|
|
13
|
+
[
|
|
14
|
+
"0x0000000000000000000000000000000000000000",
|
|
15
|
+
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
|
16
|
+
// native-asset sentinel
|
|
17
|
+
"0x4200000000000000000000000000000000000006",
|
|
18
|
+
// WETH (Base)
|
|
19
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
|
20
|
+
// USDC (Base)
|
|
21
|
+
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
|
22
|
+
// WETH (Ethereum)
|
|
23
|
+
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
|
24
|
+
// USDC (Ethereum)
|
|
25
|
+
"0xdac17f958d2ee523a2206206994597c13d831ec7",
|
|
26
|
+
// USDT (Ethereum)
|
|
27
|
+
"0x6b175474e89094c44da98b954eedeac495271d0f",
|
|
28
|
+
// DAI (Ethereum)
|
|
29
|
+
"0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c",
|
|
30
|
+
// WBNB
|
|
31
|
+
"0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d",
|
|
32
|
+
// USDC (BSC)
|
|
33
|
+
"0x55d398326f99059ff775485246999027b3197955",
|
|
34
|
+
// USDT (BSC)
|
|
35
|
+
"so11111111111111111111111111111111111111112",
|
|
36
|
+
// wrapped SOL
|
|
37
|
+
"epjfwdd5aufqssqem2qn1xzybapc8g4wegggkzwytdt1v",
|
|
38
|
+
// USDC (Solana)
|
|
39
|
+
"es9vmfrzacermjfrf4h2fyd4kconky11mcce8benwnyb"
|
|
40
|
+
// USDT (Solana)
|
|
41
|
+
]
|
|
42
|
+
);
|
|
43
|
+
var CHAIN_NAMES = {
|
|
44
|
+
base: "base",
|
|
45
|
+
"8453": "base",
|
|
46
|
+
"eip155:8453": "base",
|
|
47
|
+
ethereum: "ethereum",
|
|
48
|
+
eth: "ethereum",
|
|
49
|
+
mainnet: "ethereum",
|
|
50
|
+
"1": "ethereum",
|
|
51
|
+
"eip155:1": "ethereum",
|
|
52
|
+
bsc: "bsc",
|
|
53
|
+
bnb: "bsc",
|
|
54
|
+
binance: "bsc",
|
|
55
|
+
"56": "bsc",
|
|
56
|
+
"eip155:56": "bsc",
|
|
57
|
+
solana: "solana",
|
|
58
|
+
sol: "solana"
|
|
59
|
+
};
|
|
60
|
+
function readChain(v) {
|
|
61
|
+
if (v === void 0 || v === null || v === "") return void 0;
|
|
62
|
+
const s = String(v).toLowerCase().trim();
|
|
63
|
+
return CHAIN_NAMES[s] ?? "unsupported";
|
|
64
|
+
}
|
|
65
|
+
function norm(k) {
|
|
66
|
+
return k.toLowerCase().replace(/-/g, "_").replace(/^_+/, "");
|
|
67
|
+
}
|
|
68
|
+
function skip(token) {
|
|
69
|
+
return SKIP.has(token.toLowerCase());
|
|
70
|
+
}
|
|
71
|
+
function isEvm(s) {
|
|
72
|
+
return /^0x[a-fA-F0-9]{40}$/.test(s);
|
|
73
|
+
}
|
|
74
|
+
var IN_FLAGS = ["token-in", "input-mint", "from-token", "sell-token", "from", "in"];
|
|
75
|
+
var OUT_FLAGS = ["token-out", "output-mint", "to-token", "buy-token", "mint", "token", "ca"];
|
|
76
|
+
function commandOf(params) {
|
|
77
|
+
for (const k of ["command", "cmd", "script", "input"]) if (typeof params[k] === "string") return params[k];
|
|
78
|
+
return void 0;
|
|
79
|
+
}
|
|
80
|
+
function flags(seg, names) {
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const n of names) for (const m of seg.matchAll(new RegExp(`--${n}(?:=|\\s+)['"]?([^\\s'"]+)`, "gi"))) out.push(m[1]);
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
function detectTrade(toolName, params, defaultChain = "base") {
|
|
86
|
+
const found = [];
|
|
87
|
+
const cmd = commandOf(params);
|
|
88
|
+
if (cmd !== void 0) found.push(...fromCommand(cmd, defaultChain));
|
|
89
|
+
if (TRADE_TOOL.test(toolName)) found.push(...fromParams(params, defaultChain));
|
|
90
|
+
const seen = /* @__PURE__ */ new Set();
|
|
91
|
+
return found.filter((t) => {
|
|
92
|
+
const k = `${t.chain}:${t.token.toLowerCase()}`;
|
|
93
|
+
return seen.has(k) ? false : (seen.add(k), true);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function fromCommand(cmd, defaultChain) {
|
|
97
|
+
if (!TRADE_VERB.test(cmd)) return [];
|
|
98
|
+
const named = flags(cmd, ["chain", "network"]).map((v) => readChain(v));
|
|
99
|
+
const supported = new Set(named.filter((c) => c !== void 0 && c !== "unsupported"));
|
|
100
|
+
const unsupportedNamed = named.includes("unsupported");
|
|
101
|
+
const evmChains = [.../* @__PURE__ */ new Set([...[...supported].filter((c) => c !== "solana"), defaultChain])];
|
|
102
|
+
const unambiguous = !/#|\$'/.test(cmd) && (cmd.match(/"/g) ?? []).length % 2 === 0 && (cmd.match(/'/g) ?? []).length % 2 === 0;
|
|
103
|
+
const sold = /* @__PURE__ */ new Map();
|
|
104
|
+
if (unambiguous) {
|
|
105
|
+
const markSold = (v) => sold.set(v.toLowerCase(), (sold.get(v.toLowerCase()) ?? 0) + 1);
|
|
106
|
+
for (const v of flags(cmd, IN_FLAGS)) markSold(v);
|
|
107
|
+
for (const seg of cmd.split(/;|&&?|\|\|?|\n/)) {
|
|
108
|
+
const positional = /\bawal\b/.test(seg) ? seg.match(/\btrade\s+(\S+)\s+(\S+)\s+(\S+)/) : null;
|
|
109
|
+
if (positional) markSold(positional[2].replace(/['"]/g, ""));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const mentions = (v) => cmd.toLowerCase().split(v.toLowerCase()).length - 1;
|
|
113
|
+
const keep = (v) => !skip(v) && (sold.get(v.toLowerCase()) ?? 0) < mentions(v);
|
|
114
|
+
const out = [];
|
|
115
|
+
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 });
|
|
118
|
+
const words = [...new Set(cmd.split(/[\s;|&`()]+/).map((x) => x.replace(/^[^=]*=/, "").replace(/['"]/g, "")))];
|
|
119
|
+
if (supported.has("solana")) {
|
|
120
|
+
for (const w of words) if (SOLANA_MINT.test(w) && keep(w)) out.push({ chain: "solana", token: w });
|
|
121
|
+
} 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 });
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
function fromParams(params, defaultChain) {
|
|
127
|
+
const flat = flatten(params);
|
|
128
|
+
const chain = readChain(flat.get("chain") ?? flat.get("network") ?? flat.get("chainid") ?? flat.get("chain_id"));
|
|
129
|
+
for (const keys of [OUT_KEYS, TOKEN_KEYS]) {
|
|
130
|
+
for (const k of keys) {
|
|
131
|
+
const v = flat.get(k);
|
|
132
|
+
if (typeof v === "string") {
|
|
133
|
+
const t = target(v, chain, defaultChain, true);
|
|
134
|
+
if (t.length) return t;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
function flatten(params) {
|
|
141
|
+
const m = /* @__PURE__ */ new Map();
|
|
142
|
+
const add = (k, v) => {
|
|
143
|
+
const n = norm(k);
|
|
144
|
+
if (PARTY_KEYS.test(n)) return;
|
|
145
|
+
if (!m.has(n)) m.set(n, v);
|
|
146
|
+
const squashed = n.replace(/_/g, "");
|
|
147
|
+
if (!m.has(squashed)) m.set(squashed, v);
|
|
148
|
+
};
|
|
149
|
+
for (const [k, v] of Object.entries(params)) {
|
|
150
|
+
if (v && typeof v === "object" && !Array.isArray(v)) for (const [k2, v2] of Object.entries(v)) add(k2, v2);
|
|
151
|
+
else add(k, v);
|
|
152
|
+
}
|
|
153
|
+
return m;
|
|
154
|
+
}
|
|
155
|
+
function target(raw, chain, defaultChain, named) {
|
|
156
|
+
const v = raw.trim();
|
|
157
|
+
if (skip(v)) return [];
|
|
158
|
+
if (isEvm(v)) return [{ chain: chain === "unsupported" ? "unsupported" : chain && chain !== "solana" ? chain : defaultChain, token: v }];
|
|
159
|
+
if (named && SOLANA_MINT.test(v) && (chain === void 0 || chain === "solana")) return [{ chain: "solana", token: v }];
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/guard.ts
|
|
164
|
+
var MAX_TOKENS = 5;
|
|
165
|
+
function createGuard(check, cfg = {}, now = Date.now) {
|
|
166
|
+
const cacheMs = cfg.cacheMs ?? 10 * 6e4;
|
|
167
|
+
const allow = new Set((cfg.allowTokens ?? []).map((a) => a.toLowerCase()));
|
|
168
|
+
const cache = /* @__PURE__ */ new Map();
|
|
169
|
+
const warned = /* @__PURE__ */ new Map();
|
|
170
|
+
const warnOnce = (why) => {
|
|
171
|
+
if (!cfg.warn) return;
|
|
172
|
+
const last = warned.get(why);
|
|
173
|
+
if (last !== void 0 && now() - last < 10 * 6e4) return;
|
|
174
|
+
warned.set(why, now());
|
|
175
|
+
cfg.warn(`true402 swap guard: a token could not be checked (${why}); the trade was ${cfg.onUnchecked === "block" ? "blocked" : cfg.onUnchecked === "ask" ? "held for approval" : "allowed unchecked"}.`);
|
|
176
|
+
};
|
|
177
|
+
async function verdict(t, signal) {
|
|
178
|
+
const key = `${t.chain}:${t.token.toLowerCase()}`;
|
|
179
|
+
const hit = cache.get(key);
|
|
180
|
+
if (hit && now() - hit.at < cacheMs) return hit.v;
|
|
181
|
+
let v;
|
|
182
|
+
try {
|
|
183
|
+
v = await check(t, signal);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
v = { level: "unchecked", reasons: [clean(e instanceof Error ? e.message : String(e))] };
|
|
186
|
+
}
|
|
187
|
+
if (v.level !== "unchecked") cache.set(key, { at: now(), v });
|
|
188
|
+
return v;
|
|
189
|
+
}
|
|
190
|
+
return async function beforeToolCall(toolName, params, signal) {
|
|
191
|
+
const targets = detectTrade(toolName, params, cfg.defaultChain ?? "base").filter((t) => !allow.has(t.token.toLowerCase()));
|
|
192
|
+
if (!targets.length) return void 0;
|
|
193
|
+
const byToken = /* @__PURE__ */ new Map();
|
|
194
|
+
for (const t of targets) {
|
|
195
|
+
const k = t.token.toLowerCase();
|
|
196
|
+
byToken.set(k, [...byToken.get(k) ?? [], t]);
|
|
197
|
+
}
|
|
198
|
+
const groups = [...byToken.values()];
|
|
199
|
+
if (groups.length > MAX_TOKENS) {
|
|
200
|
+
const why = `this call names ${groups.length} tokens; the guard checks at most ${MAX_TOKENS} per call`;
|
|
201
|
+
warnOnce(why);
|
|
202
|
+
if (cfg.onUnchecked === "block") return { block: true, blockReason: `true402 swap guard blocked this trade: ${why}.` };
|
|
203
|
+
return { requireApproval: { title: "Too many tokens to check \u2014 confirm the trade", description: `${why}.`, severity: "warning" } };
|
|
204
|
+
}
|
|
205
|
+
const results = await Promise.all(
|
|
206
|
+
groups.map(async (ts) => {
|
|
207
|
+
const vs = await Promise.all(
|
|
208
|
+
ts.map((t) => t.chain === "unsupported" ? Promise.resolve({ level: "unchecked", reasons: ["a chain true402 does not serve"] }) : verdict(t, signal))
|
|
209
|
+
);
|
|
210
|
+
const pick = (level) => vs.findIndex((x) => x.level === level);
|
|
211
|
+
if (pick("danger") >= 0) return { t: ts[pick("danger")], v: vs[pick("danger")] };
|
|
212
|
+
const u = ts.findIndex((t) => t.chain === "unsupported");
|
|
213
|
+
if (u >= 0) return { t: ts[u], v: vs[u] };
|
|
214
|
+
const failed = vs.findIndex((x) => x.level === "unchecked" && !x.notToken);
|
|
215
|
+
if (failed >= 0) return { t: ts[failed], v: vs[failed] };
|
|
216
|
+
for (const level of ["caution", "ok"]) {
|
|
217
|
+
const i = pick(level);
|
|
218
|
+
if (i >= 0) return { t: ts[i], v: vs[i] };
|
|
219
|
+
}
|
|
220
|
+
return { t: ts[0], v: { level: "unchecked", reasons: [...new Set(vs.flatMap((x) => x.reasons))] } };
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
const danger = results.find((r) => r.v.level === "danger");
|
|
224
|
+
if (danger) {
|
|
225
|
+
return {
|
|
226
|
+
block: true,
|
|
227
|
+
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
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const caution = results.find((r) => r.v.level === "caution");
|
|
231
|
+
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
|
+
};
|
|
239
|
+
}
|
|
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" } };
|
|
246
|
+
}
|
|
247
|
+
return void 0;
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function reasonText(v) {
|
|
251
|
+
return v.reasons.length ? ` (${v.reasons.slice(0, 4).join("; ")})` : "";
|
|
252
|
+
}
|
|
253
|
+
function clean(s, max = 160) {
|
|
254
|
+
const t = s.replace(/[\u0000-\u001f\u007f-\u009f---]/g, " ").replace(/\s+/g, " ").trim();
|
|
255
|
+
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/x402.ts
|
|
259
|
+
import { randomBytes } from "crypto";
|
|
260
|
+
import { createPublicClient, http, getAddress } from "viem";
|
|
261
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
262
|
+
import { base } from "viem/chains";
|
|
263
|
+
|
|
264
|
+
// 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" };
|
|
266
|
+
|
|
267
|
+
// src/x402.ts
|
|
268
|
+
var USDC_ABI = [
|
|
269
|
+
{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "a", type: "address" }], outputs: [{ type: "uint256" }] }
|
|
270
|
+
];
|
|
271
|
+
var BASE_USDC = getAddress("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913");
|
|
272
|
+
var BASE_NETWORK = "eip155:8453";
|
|
273
|
+
var MIN_AUTH_WINDOW_S = 30;
|
|
274
|
+
var MAX_AUTH_WINDOW_S = 600;
|
|
275
|
+
var DEFAULT_AUTH_WINDOW_S = 120;
|
|
276
|
+
function authWindowSeconds(raw) {
|
|
277
|
+
const n = typeof raw === "string" || typeof raw === "number" ? Number(raw) : NaN;
|
|
278
|
+
if (!Number.isInteger(n) || n <= 0) return DEFAULT_AUTH_WINDOW_S;
|
|
279
|
+
return Math.min(MAX_AUTH_WINDOW_S, Math.max(MIN_AUTH_WINDOW_S, n));
|
|
280
|
+
}
|
|
281
|
+
var USER_AGENT = `true402-${CLIENT.id}/${CLIENT.version} (+https://true402.dev/integrations)`;
|
|
282
|
+
var updateNoticeShown = false;
|
|
283
|
+
var DEFAULT_ORIGIN = "https://true402.dev/api";
|
|
284
|
+
var isTrue402 = (url) => {
|
|
285
|
+
try {
|
|
286
|
+
const host = new URL(url).hostname;
|
|
287
|
+
return host === "true402.dev" || host.endsWith(".true402.dev");
|
|
288
|
+
} catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
var isNewer = (a, b) => {
|
|
293
|
+
const x = a.split(".").map(Number);
|
|
294
|
+
const y = b.split(".").map(Number);
|
|
295
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i];
|
|
296
|
+
return false;
|
|
297
|
+
};
|
|
298
|
+
function updateNotice(res, fromUrl = DEFAULT_ORIGIN) {
|
|
299
|
+
if (!isTrue402(fromUrl)) return null;
|
|
300
|
+
let latest = "";
|
|
301
|
+
let required = false;
|
|
302
|
+
try {
|
|
303
|
+
latest = res.headers.get("x-true402-client-latest")?.trim() ?? "";
|
|
304
|
+
required = res.headers.get("x-true402-client-update") === "required";
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
if (!/^\d{1,4}\.\d{1,4}\.\d{1,4}$/.test(latest) || !isNewer(latest, CLIENT.version)) return null;
|
|
309
|
+
return required ? `[true402] ${CLIENT.package} ${CLIENT.version} has a security fix pending \u2014 update to ${latest}: ${CLIENT.install}` : `[true402] ${CLIENT.package} ${latest} is available (you have ${CLIENT.version}): ${CLIENT.install}`;
|
|
310
|
+
}
|
|
311
|
+
function noteUpdate(res, fromUrl) {
|
|
312
|
+
if (updateNoticeShown) return;
|
|
313
|
+
const line = updateNotice(res, fromUrl);
|
|
314
|
+
if (!line) return;
|
|
315
|
+
updateNoticeShown = true;
|
|
316
|
+
console.warn(line);
|
|
317
|
+
}
|
|
318
|
+
var PROBE_TIMEOUT_MS = 3e4;
|
|
319
|
+
var PAID_TIMEOUT_MS = 12e4;
|
|
320
|
+
async function serverReason(res) {
|
|
321
|
+
try {
|
|
322
|
+
const text = (await res.text()).trim();
|
|
323
|
+
return text ? ` \u2014 ${text.slice(0, 300)}` : "";
|
|
324
|
+
} catch {
|
|
325
|
+
return "";
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function isSecureUrl(url) {
|
|
329
|
+
let u;
|
|
330
|
+
try {
|
|
331
|
+
u = new URL(url);
|
|
332
|
+
} catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
if (u.protocol === "https:") return true;
|
|
336
|
+
return u.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(u.hostname);
|
|
337
|
+
}
|
|
338
|
+
function refuseRedirect(res, url) {
|
|
339
|
+
if (String(res.type) === "opaqueredirect" || res.status >= 300 && res.status < 400)
|
|
340
|
+
throw new Error(`${url} answered HTTP ${res.status}: server redirected; refusing to send a payment elsewhere`);
|
|
341
|
+
}
|
|
342
|
+
async function payStall(path, payload, opts) {
|
|
343
|
+
const baseUrl = opts.baseUrl ?? "https://true402.dev/api";
|
|
344
|
+
const rpcUrl = opts.rpcUrl ?? "https://mainnet.base.org";
|
|
345
|
+
const key = opts.payerPrivateKey.startsWith("0x") ? opts.payerPrivateKey : `0x${opts.payerPrivateKey}`;
|
|
346
|
+
const account = privateKeyToAccount(key);
|
|
347
|
+
const url = `${baseUrl}${path}`;
|
|
348
|
+
const body = JSON.stringify(payload);
|
|
349
|
+
const first = await fetch(url, {
|
|
350
|
+
method: "POST",
|
|
351
|
+
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
352
|
+
body,
|
|
353
|
+
redirect: "manual",
|
|
354
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
355
|
+
});
|
|
356
|
+
noteUpdate(first, url);
|
|
357
|
+
refuseRedirect(first, url);
|
|
358
|
+
if (first.ok) return await first.json();
|
|
359
|
+
if (first.status !== 402)
|
|
360
|
+
throw new Error(`expected HTTP 402 from ${path}, got ${first.status}${await serverReason(first)}`);
|
|
361
|
+
const challenge = await first.json();
|
|
362
|
+
const req = (challenge.accepts ?? []).find((a) => a.scheme === "exact" && a.network === BASE_NETWORK);
|
|
363
|
+
if (!req)
|
|
364
|
+
throw new Error(
|
|
365
|
+
`no payable x402 requirement in the 402 \u2014 this client settles USDC on ${BASE_NETWORK} only`
|
|
366
|
+
);
|
|
367
|
+
if (req.network !== BASE_NETWORK) throw new Error(`unexpected payment network "${req.network}" (expected ${BASE_NETWORK}) \u2014 refusing to sign`);
|
|
368
|
+
const usdc = getAddress(req.asset);
|
|
369
|
+
if (usdc !== BASE_USDC) throw new Error(`unexpected payment asset ${usdc} (expected Base USDC) \u2014 refusing to sign`);
|
|
370
|
+
const rawAmount = req.amount ?? req.maxAmountRequired;
|
|
371
|
+
if (rawAmount === void 0 || !/^\d+$/.test(String(rawAmount)) || BigInt(rawAmount) <= 0n)
|
|
372
|
+
throw new Error("402 carries no positive payment amount \u2014 refusing to sign");
|
|
373
|
+
const value = BigInt(rawAmount);
|
|
374
|
+
const capAtomic = BigInt(Math.round((opts.maxAmountUsd ?? 0.25) * 1e6));
|
|
375
|
+
if (value > capAtomic) throw new Error(`402 demands ${value} USDC base units, over the $${opts.maxAmountUsd ?? 0.25} cap (maxAmountUsd) \u2014 refusing to sign`);
|
|
376
|
+
const pub = createPublicClient({ chain: base, transport: http(rpcUrl) });
|
|
377
|
+
const held = await pub.readContract({ address: usdc, abi: USDC_ABI, functionName: "balanceOf", args: [account.address] }).catch(() => null);
|
|
378
|
+
if (held !== null && held < value) throw new Error(`payer ${account.address} holds ${held} < ${value} USDC base units \u2014 fund it`);
|
|
379
|
+
if (!isSecureUrl(url)) {
|
|
380
|
+
throw new Error(`refusing to send a signed payment over cleartext (${url}) \u2014 use https`);
|
|
381
|
+
}
|
|
382
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
383
|
+
const authorization = {
|
|
384
|
+
from: account.address,
|
|
385
|
+
to: getAddress(req.payTo),
|
|
386
|
+
value,
|
|
387
|
+
validAfter: BigInt(now - 60),
|
|
388
|
+
validBefore: BigInt(now + authWindowSeconds(req.maxTimeoutSeconds)),
|
|
389
|
+
nonce: `0x${randomBytes(32).toString("hex")}`
|
|
390
|
+
};
|
|
391
|
+
const signature = await account.signTypedData({
|
|
392
|
+
domain: { name: req.extra?.name ?? "USD Coin", version: req.extra?.version ?? "2", chainId: base.id, verifyingContract: usdc },
|
|
393
|
+
types: {
|
|
394
|
+
TransferWithAuthorization: [
|
|
395
|
+
{ name: "from", type: "address" },
|
|
396
|
+
{ name: "to", type: "address" },
|
|
397
|
+
{ name: "value", type: "uint256" },
|
|
398
|
+
{ name: "validAfter", type: "uint256" },
|
|
399
|
+
{ name: "validBefore", type: "uint256" },
|
|
400
|
+
{ name: "nonce", type: "bytes32" }
|
|
401
|
+
]
|
|
402
|
+
},
|
|
403
|
+
primaryType: "TransferWithAuthorization",
|
|
404
|
+
message: authorization
|
|
405
|
+
});
|
|
406
|
+
const xPayment = Buffer.from(JSON.stringify({
|
|
407
|
+
x402Version: 2,
|
|
408
|
+
accepted: req,
|
|
409
|
+
payload: {
|
|
410
|
+
signature,
|
|
411
|
+
authorization: {
|
|
412
|
+
from: authorization.from,
|
|
413
|
+
to: authorization.to,
|
|
414
|
+
value: value.toString(),
|
|
415
|
+
validAfter: authorization.validAfter.toString(),
|
|
416
|
+
validBefore: authorization.validBefore.toString(),
|
|
417
|
+
nonce: authorization.nonce
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
})).toString("base64");
|
|
421
|
+
const paid = await fetch(url, {
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: { "content-type": "application/json", "user-agent": USER_AGENT, "PAYMENT-SIGNATURE": xPayment },
|
|
424
|
+
body,
|
|
425
|
+
redirect: "manual",
|
|
426
|
+
signal: AbortSignal.timeout(PAID_TIMEOUT_MS)
|
|
427
|
+
});
|
|
428
|
+
noteUpdate(paid, url);
|
|
429
|
+
refuseRedirect(paid, url);
|
|
430
|
+
if (!paid.ok)
|
|
431
|
+
throw new Error(
|
|
432
|
+
`paid request to ${path} failed (HTTP ${paid.status}): the server reported failure \u2014 a seller following x402 settles only on a 2xx${await serverReason(paid)}`
|
|
433
|
+
);
|
|
434
|
+
return await paid.json();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/check.ts
|
|
438
|
+
var NOT_A_TOKEN = /* @__PURE__ */ new Set(["not_a_contract", "mint_unreadable", "mint_not_initialized"]);
|
|
439
|
+
var SOL_NEVER_SELLABLE = /* @__PURE__ */ new Set(["non_transferable", "permanent_delegate"]);
|
|
440
|
+
var SOL_SELL_BLOCKABLE = /* @__PURE__ */ new Set(["freeze_authority_active", "pausable", "transfer_hook", "default_frozen"]);
|
|
441
|
+
function evmVerdict(r, chain) {
|
|
442
|
+
const flags2 = strings(r.safety?.flags);
|
|
443
|
+
if (flags2.some((f) => NOT_A_TOKEN.has(f))) return { level: "unchecked", notToken: true, reasons: [`no token contract at this address on ${chain}`] };
|
|
444
|
+
const reasons = strings(r.verdict?.reasons).map((s) => clean(s));
|
|
445
|
+
switch (r.verdict?.rating) {
|
|
446
|
+
case "avoid":
|
|
447
|
+
return { level: "danger", reasons };
|
|
448
|
+
case "caution":
|
|
449
|
+
return { level: "caution", reasons };
|
|
450
|
+
case "ok":
|
|
451
|
+
return { level: "ok", reasons };
|
|
452
|
+
default:
|
|
453
|
+
return { level: "unchecked", reasons: ["no verdict in the answer"] };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function solanaVerdict(r) {
|
|
457
|
+
const flags2 = strings(r.flags);
|
|
458
|
+
if (flags2.some((f) => NOT_A_TOKEN.has(f))) return { level: "unchecked", notToken: true, reasons: ["not a token mint on solana"] };
|
|
459
|
+
const reasons = flags2.map((f) => clean(f.replace(/_/g, " ")));
|
|
460
|
+
if (flags2.some((f) => SOL_NEVER_SELLABLE.has(f))) return { level: "danger", reasons };
|
|
461
|
+
const canStopSell = flags2.some((f) => SOL_SELL_BLOCKABLE.has(f));
|
|
462
|
+
switch (r.risk) {
|
|
463
|
+
case "critical":
|
|
464
|
+
return { level: "danger", reasons };
|
|
465
|
+
case "high":
|
|
466
|
+
return { level: canStopSell ? "danger" : "caution", reasons };
|
|
467
|
+
case "medium":
|
|
468
|
+
return { level: canStopSell ? "caution" : "ok", reasons };
|
|
469
|
+
case "low":
|
|
470
|
+
return { level: canStopSell ? "caution" : "ok", reasons };
|
|
471
|
+
default:
|
|
472
|
+
return { level: "unchecked", reasons: ["no risk level in the answer"] };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function strings(v) {
|
|
476
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
477
|
+
}
|
|
478
|
+
function createChecker(cfg = {}) {
|
|
479
|
+
const baseUrl = cfg.baseUrl ?? "https://true402.dev/api";
|
|
480
|
+
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
|
+
}
|
|
485
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
486
|
+
method: "POST",
|
|
487
|
+
headers: { "content-type": "application/json", "user-agent": `true402-${CLIENT.id}/${CLIENT.version} (+https://true402.dev)` },
|
|
488
|
+
body: JSON.stringify({ token }),
|
|
489
|
+
redirect: "manual",
|
|
490
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
491
|
+
});
|
|
492
|
+
if (res.status === 402) throw new Error("the free daily checks are used up \u2014 set PAYER_PRIVATE_KEY to pay per check");
|
|
493
|
+
if (!res.ok) throw new Error(`true402 answered HTTP ${res.status}`);
|
|
494
|
+
return res.json();
|
|
495
|
+
}
|
|
496
|
+
return async (t) => {
|
|
497
|
+
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
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/index.ts
|
|
512
|
+
var CHAINS = /* @__PURE__ */ new Set(["base", "ethereum", "bsc"]);
|
|
513
|
+
function readConfig(raw) {
|
|
514
|
+
const c = raw && typeof raw === "object" ? raw : {};
|
|
515
|
+
const pick = (v, allowed) => allowed.includes(v) ? v : void 0;
|
|
516
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
|
|
517
|
+
return {
|
|
518
|
+
defaultChain: CHAINS.has(c.defaultChain) ? c.defaultChain : void 0,
|
|
519
|
+
onCaution: pick(c.onCaution, ["allow", "ask"]),
|
|
520
|
+
onUnchecked: pick(c.onUnchecked, ["allow", "ask", "block"]),
|
|
521
|
+
allowTokens: Array.isArray(c.allowTokens) ? c.allowTokens.filter((x) => typeof x === "string") : void 0,
|
|
522
|
+
timeoutMs: num(c.timeoutMs),
|
|
523
|
+
maxAmountUsd: num(c.maxAmountUsd)
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
var index_default = definePluginEntry({
|
|
527
|
+
id: "true402-swap-guard",
|
|
528
|
+
name: "true402 swap guard",
|
|
529
|
+
description: "Blocks token buys when true402 finds a honeypot or a rug in progress (Base, Ethereum, BSC, Solana).",
|
|
530
|
+
register(api) {
|
|
531
|
+
const cfg = readConfig(api.pluginConfig);
|
|
532
|
+
const payerPrivateKey = process.env.TRUE402_PAYER_PRIVATE_KEY || process.env.PAYER_PRIVATE_KEY || void 0;
|
|
533
|
+
const warn = (m) => api.logger?.warn ? api.logger.warn(m) : console.warn(m);
|
|
534
|
+
const guard = createGuard(createChecker({ ...cfg, payerPrivateKey }), { ...cfg, warn });
|
|
535
|
+
api.on("before_tool_call", (event, ctx) => guard(event.toolName, event.params ?? {}, ctx?.abortSignal), { priority: 60 });
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
export {
|
|
539
|
+
index_default as default,
|
|
540
|
+
readConfig
|
|
541
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "true402-swap-guard",
|
|
3
|
+
"name": "true402 swap guard",
|
|
4
|
+
"description": "Blocks token buys when true402 finds a honeypot or a rug in progress (Base, Ethereum, BSC, Solana).",
|
|
5
|
+
"categories": ["security"],
|
|
6
|
+
"activation": {
|
|
7
|
+
"onStartup": true
|
|
8
|
+
},
|
|
9
|
+
"configSchema": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": false,
|
|
12
|
+
"properties": {
|
|
13
|
+
"defaultChain": { "type": "string", "enum": ["base", "ethereum", "bsc"], "description": "Chain assumed for a 0x address when the trade names none. Default base." },
|
|
14
|
+
"onCaution": { "type": "string", "enum": ["allow", "ask"], "description": "On a 'caution' verdict: let the trade through (default) or ask you first." },
|
|
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
|
+
"allowTokens": { "type": "array", "items": { "type": "string" }, "description": "Token addresses never checked." },
|
|
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)." }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@true402.dev/openclaw-swap-guard",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"openclaw.plugin.json"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup src/index.ts --format esm --clean --external openclaw",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"openclaw",
|
|
18
|
+
"openclaw-plugin",
|
|
19
|
+
"x402",
|
|
20
|
+
"honeypot",
|
|
21
|
+
"rug-check",
|
|
22
|
+
"token-safety",
|
|
23
|
+
"swap",
|
|
24
|
+
"trading-agent",
|
|
25
|
+
"base",
|
|
26
|
+
"solana",
|
|
27
|
+
"ai-agents"
|
|
28
|
+
],
|
|
29
|
+
"homepage": "https://true402.dev",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/true402/openclaw-swap-guard"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"viem": "^2.21.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"openclaw": ">=2026.10.5"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"openclaw": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"tsup": "^8.0.0",
|
|
48
|
+
"typescript": "^5.4.0"
|
|
49
|
+
},
|
|
50
|
+
"openclaw": {
|
|
51
|
+
"extensions": [
|
|
52
|
+
"./dist/index.js"
|
|
53
|
+
],
|
|
54
|
+
"install": {
|
|
55
|
+
"clawhubSpec": "clawhub:@true402.dev/openclaw-swap-guard",
|
|
56
|
+
"npmSpec": "@true402.dev/openclaw-swap-guard",
|
|
57
|
+
"defaultChoice": "npm",
|
|
58
|
+
"minHostVersion": ">=2026.10.5"
|
|
59
|
+
},
|
|
60
|
+
"compat": {
|
|
61
|
+
"pluginApi": ">=2026.10.5"
|
|
62
|
+
},
|
|
63
|
+
"build": {
|
|
64
|
+
"openclawVersion": "2026.10.5"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public"
|
|
69
|
+
},
|
|
70
|
+
"allowScripts": {
|
|
71
|
+
"esbuild": true
|
|
72
|
+
}
|
|
73
|
+
}
|