devx-address-poisoning-check 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 +61 -2
- package/bin/cli.js +22 -0
- package/package.json +17 -4
- package/src/index.js +50 -0
- package/src/mcp.js +12 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Made by DevX
|
|
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,62 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Address Poisoning Check
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Is this the address you meant?** Address poisoning plants a lookalike address (same first and last characters, different middle) in a wallet's history with a zero-value, dust or fake-token transfer, and waits for the owner (or their AI agent) to copy it. Bitquery counted about **$76M stolen on Ethereum alone in the 12 months to July 2026**.
|
|
4
|
+
|
|
5
|
+
This repo has three pieces, all read-only (we never ask for keys and never move funds):
|
|
6
|
+
|
|
7
|
+
| | What | Cost |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| **CLI / stdio MCP** `devx-address-poisoning-check` | Offline: is the recipient a lookalike of any address you trust? Is its EIP-55 checksum valid? Nothing leaves your machine. | free |
|
|
10
|
+
| **Remote MCP** `https://address-poisoning-check.nextill-pos.workers.dev/mcp` | Same offline check as a hosted MCP tool | free |
|
|
11
|
+
| **HTTP API** `GET /v1/check?from=<sender>&to=<recipient>` | On-chain: scans the sender's real transfer history (Ethereum + Base, optional Avalanche) and flags lookalikes of real counterparties, zero-value / dust / fake-token poisoning transfers (with tx hashes), recipients that only ever appeared through such transfers, and senders that were already poisoned. Returns `ok \| caution \| danger` and the address you probably meant. | 0.005 USDC via [x402](https://x402.org) on Base |
|
|
12
|
+
|
|
13
|
+
Live: https://address-poisoning-check.nextill-pos.workers.dev
|
|
14
|
+
|
|
15
|
+
## CLI
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npx devx-address-poisoning-check 0xd9A1C3788D81257612E2581A6ea0aDa244853a91 --known 0xd9A1b0B1e1aE382DbDc898Ea68012FfcB2853a91
|
|
19
|
+
# DANGER: Looks like 0xd9a1...3a91 (same first 4 / last 6 hex chars) but is a different address. Classic address-poisoning lookalike.
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Exit code `2` on danger, so it can gate scripts and agent payment flows.
|
|
23
|
+
|
|
24
|
+
## MCP
|
|
25
|
+
|
|
26
|
+
Claude Desktop / Cursor (stdio):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{ "mcpServers": { "address-poisoning-check": { "command": "npx", "args": ["-y", "devx-address-poisoning-check", "mcp"] } } }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or remote (streamable HTTP): `https://address-poisoning-check.nextill-pos.workers.dev/mcp`
|
|
33
|
+
|
|
34
|
+
Tools: `check_recipient_lookalike` (free), `onchain_check_info` (how to buy the on-chain check; does not pay).
|
|
35
|
+
|
|
36
|
+
## Paid on-chain check (x402)
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
GET https://address-poisoning-check.nextill-pos.workers.dev/v1/check?from=0x...&to=0x...[&chains=ethereum,base,avalanche]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
* Answers `402` with an x402 v1 body (`X-PAYMENT`, network `base`) and a v2 `PAYMENT-REQUIRED` header (`PAYMENT-SIGNATURE`, `eip155:8453`). Both work.
|
|
43
|
+
* 5000 atomic USDC (0.005 USDC) to `0xB3cDDccEe6B150c8efFD55B961afc0Ecefe7f247`, settled through the Coinbase CDP facilitator, listed in the CDP x402 Bazaar.
|
|
44
|
+
* Settled only **after** the report is produced; bad input or data-source errors are not charged.
|
|
45
|
+
* Data: Ethereum/Avalanche via the Routescan explorer API (last 300 token transfers + 150 transactions of the sender); Base via public RPC logs (about the last 4-5 days of USDC, USDbC, USDT, EURC, WETH, cbBTC transfers). It is a heuristic over a window of public data: always confirm the full address with the payee.
|
|
46
|
+
|
|
47
|
+
Example (the May 2024 $68M WBTC case): `from=0x1E227979f0b5BC691a70DEAed2e0F39a6F538FD5&to=0xd9A1C3788D81257612E2581A6ea0aDa244853a91` returns `danger` / `possibly_already_poisoned`: the victim did send real value to that address, but it first appeared in their history through a zero-value transfer.
|
|
48
|
+
|
|
49
|
+
OpenAPI: [/openapi.json](https://address-poisoning-check.nextill-pos.workers.dev/openapi.json) · Terms: [/terms](https://address-poisoning-check.nextill-pos.workers.dev/terms)
|
|
50
|
+
|
|
51
|
+
## Related DevX tools
|
|
52
|
+
|
|
53
|
+
* [7702 Sweeper Check](https://github.com/devx-agent/7702-sweeper-check): is an EIP-7702 sweeper delegation set on a wallet?
|
|
54
|
+
* [Crypto Link Check](https://github.com/devx-agent/crypto-link-check): is a crypto URL a phishing / drainer lookalike?
|
|
55
|
+
* [Repo Run Check](https://github.com/devx-agent/repo-run-check): is a GitHub repo safe to clone and run?
|
|
56
|
+
|
|
57
|
+
## Layout
|
|
58
|
+
|
|
59
|
+
* `cli/`: npm package `devx-address-poisoning-check` (CLI + stdio MCP), `npm test`
|
|
60
|
+
* `worker/`: the Cloudflare Worker behind the hosted API (`npm run build`)
|
|
61
|
+
|
|
62
|
+
MIT. Made by [DevX](https://madebydevx.com) · madebydevx@gmail.com
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { checkRecipient, onchainPrice } from "../src/index.js";
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
if (args[0] === "mcp") {
|
|
5
|
+
const { createServer } = await import("../src/mcp.js");
|
|
6
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7
|
+
await createServer().connect(new StdioServerTransport());
|
|
8
|
+
} else if (!args.length || args[0] === "-h" || args[0] === "--help") {
|
|
9
|
+
process.stdout.write(`Usage:
|
|
10
|
+
devx-address-poisoning-check <recipient> --known 0xA,0xB [--json] offline lookalike + checksum check
|
|
11
|
+
devx-address-poisoning-check onchain <from> <to> price/URL of the hosted on-chain check (does not pay)
|
|
12
|
+
devx-address-poisoning-check mcp run as an MCP server (stdio)
|
|
13
|
+
Exit codes: 0 match/unknown, 2 danger, 1 error. Never asks for keys.
|
|
14
|
+
`);
|
|
15
|
+
} else if (args[0] === "onchain") {
|
|
16
|
+
try { process.stdout.write(JSON.stringify(await onchainPrice(args[1], args[2]), null, 2) + "\n"); } catch (e) { process.stderr.write(e.message + "\n"); process.exitCode = 1; }
|
|
17
|
+
} else {
|
|
18
|
+
const ki = args.indexOf("--known"); const known = ki >= 0 ? String(args[ki + 1] || "").split(/[\s,]+/).filter(Boolean) : [];
|
|
19
|
+
const json = args.includes("--json");
|
|
20
|
+
try { const r = checkRecipient(args[0], known); process.stdout.write(json ? JSON.stringify(r, null, 2) + "\n" : `${r.verdict.toUpperCase()}: ${r.note}\n${r.lookalikes.map((l) => " lookalike of " + l.address).join("\n")}\n`); process.exitCode = r.verdict === "danger" ? 2 : 0; }
|
|
21
|
+
catch (e) { process.stderr.write(e.message + "\n"); process.exitCode = 1; }
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devx-address-poisoning-check",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Free CLI + MCP server: before you send crypto, check the recipient for address poisoning (lookalike of an address you trust, bad EIP-55 checksum). Offline, no keys. Optional hosted on-chain check via x402.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Made by DevX (madebydevx.com)",
|
|
8
|
+
"repository": { "type": "git", "url": "git+https://github.com/devx-agent/address-poisoning-check.git" },
|
|
9
|
+
"homepage": "https://address-poisoning-check.nextill-pos.workers.dev",
|
|
10
|
+
"bugs": { "url": "https://github.com/devx-agent/address-poisoning-check/issues" },
|
|
11
|
+
"keywords": ["address-poisoning", "wallet-security", "ethereum", "base", "usdc", "mcp", "x402", "agents", "lookalike", "scam"],
|
|
12
|
+
"engines": { "node": ">=20" },
|
|
13
|
+
"main": "./src/index.js",
|
|
14
|
+
"bin": { "devx-address-poisoning-check": "bin/cli.js" },
|
|
15
|
+
"files": ["src", "bin", "README.md", "LICENSE"],
|
|
16
|
+
"scripts": { "test": "node --test test/*.test.js" },
|
|
17
|
+
"mcpName": "io.github.devx-agent/address-poisoning-check",
|
|
18
|
+
"dependencies": { "@modelcontextprotocol/sdk": "^1.32.1", "@noble/hashes": "^1.8.0", "zod": "^3.25.0" }
|
|
19
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// devx-address-poisoning-check: offline lookalike + EIP-55 checks, and a client for the hosted on-chain check.
|
|
2
|
+
import { keccak_256 } from "@noble/hashes/sha3";
|
|
3
|
+
export const DEFAULT_BASE = "https://address-poisoning-check.nextill-pos.workers.dev";
|
|
4
|
+
export const ADDR_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
5
|
+
const lc = (a) => String(a || "").trim().toLowerCase();
|
|
6
|
+
export const isAddr = (a) => ADDR_RE.test(String(a || "").trim());
|
|
7
|
+
|
|
8
|
+
/** Shared leading / trailing hex characters (after 0x). */
|
|
9
|
+
export function similarity(a, b) {
|
|
10
|
+
a = lc(a).slice(2); b = lc(b).slice(2);
|
|
11
|
+
let prefix = 0; while (prefix < 40 && a[prefix] === b[prefix]) prefix++;
|
|
12
|
+
let suffix = 0; while (suffix < 40 && a[39 - suffix] === b[39 - suffix]) suffix++;
|
|
13
|
+
return { prefix, suffix, identical: a === b };
|
|
14
|
+
}
|
|
15
|
+
/** True when b looks like a (same first/last characters, different middle): the shape address poisoners generate. */
|
|
16
|
+
export function isLookalike(a, b) {
|
|
17
|
+
const s = similarity(a, b); if (s.identical) return false;
|
|
18
|
+
return (s.prefix >= 3 && s.suffix >= 3) || (s.prefix + s.suffix >= 8 && Math.min(s.prefix, s.suffix) >= 2);
|
|
19
|
+
}
|
|
20
|
+
/** EIP-55: true valid, false invalid, null when the address is all lower/upper case (no checksum). */
|
|
21
|
+
export function checksumOk(a) {
|
|
22
|
+
a = String(a).trim(); const h = a.slice(2);
|
|
23
|
+
if (h === h.toLowerCase() || h === h.toUpperCase()) return null;
|
|
24
|
+
const hash = Array.from(keccak_256(new TextEncoder().encode(h.toLowerCase())), (x) => x.toString(16).padStart(2, "0")).join("");
|
|
25
|
+
for (let i = 0; i < 40; i++) { const c = h[i]; if (/[a-f]/i.test(c) && ((parseInt(hash[i], 16) >= 8) !== (c === c.toUpperCase()))) return false; }
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const short = (a) => lc(a).slice(0, 6) + "..." + lc(a).slice(-4);
|
|
29
|
+
|
|
30
|
+
/** Offline check of a recipient against addresses you trust. Nothing leaves your machine. */
|
|
31
|
+
export function checkRecipient(to, known = []) {
|
|
32
|
+
if (!isAddr(to)) throw new Error("to must be 0x + 40 hex characters");
|
|
33
|
+
const bad = known.find((k) => !isAddr(k)); if (bad) throw new Error("invalid known address: " + bad);
|
|
34
|
+
const out = { to: lc(to), checksum_valid: checksumOk(to), compared: known.length, matches: [], lookalikes: [] };
|
|
35
|
+
for (const k of known) { const s = similarity(to, k); if (s.identical) out.matches.push(lc(k)); else if (isLookalike(to, k)) out.lookalikes.push({ address: lc(k), shared_prefix: s.prefix, shared_suffix: s.suffix, shown_as: short(k) }); }
|
|
36
|
+
out.verdict = out.checksum_valid === false ? "danger" : out.lookalikes.length && !out.matches.length ? "danger" : out.matches.length ? "match" : "unknown";
|
|
37
|
+
out.note = out.verdict === "danger" ? (out.checksum_valid === false ? "Checksum is invalid: the address was mistyped or altered." : `Looks like ${out.lookalikes[0].shown_as} (same first ${out.lookalikes[0].shared_prefix} / last ${out.lookalikes[0].shared_suffix} hex chars) but is a different address. Classic address-poisoning lookalike.`)
|
|
38
|
+
: out.verdict === "match" ? "Exactly matches an address you supplied." : "No lookalike among the supplied addresses.";
|
|
39
|
+
out.onchain_check = `${DEFAULT_BASE}/v1/check?from=<your wallet>&to=${lc(to)}`;
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Info for the paid on-chain check (x402). Fetches the 402 challenge, does not pay. */
|
|
44
|
+
export async function onchainPrice(from, to, { baseUrl = DEFAULT_BASE, fetch: f = globalThis.fetch } = {}) {
|
|
45
|
+
if (!isAddr(from) || !isAddr(to)) throw new Error("from and to must be 0x + 40 hex characters");
|
|
46
|
+
const url = `${baseUrl.replace(/\/$/, "")}/v1/check?from=${lc(from)}&to=${lc(to)}`;
|
|
47
|
+
const r = await f(url, { headers: { accept: "application/json", "user-agent": "devx-address-poisoning-check" } });
|
|
48
|
+
const body = await r.json(); const a = (body.accepts || [])[0] || {};
|
|
49
|
+
return { url, status: r.status, price_usdc: Number(a.maxAmountRequired || a.amount || 0) / 1e6, payTo: a.payTo, network: a.network, note: "Pay with any x402 client (v1 X-PAYMENT or v2 PAYMENT-SIGNATURE). Settled only after the report is produced." };
|
|
50
|
+
}
|
package/src/mcp.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { checkRecipient, DEFAULT_BASE } from "./index.js";
|
|
4
|
+
export function createServer() {
|
|
5
|
+
const server = new McpServer({ name: "devx-address-poisoning-check", version: "1.0.0" }, { instructions: "Before sending crypto, check the recipient for address poisoning (lookalike of an address you trust, bad EIP-55 checksum). Runs offline; never asks for keys." });
|
|
6
|
+
server.tool("check_recipient_lookalike", "Free, offline: is the recipient a lookalike (same first/last hex chars, different middle) of any trusted address, and is its EIP-55 checksum valid? Call before every transfer to a pasted or copied address.",
|
|
7
|
+
{ to: z.string().regex(/^0x[0-9a-fA-F]{40}$/).describe("recipient you are about to pay"), known: z.array(z.string().regex(/^0x[0-9a-fA-F]{40}$/)).max(200).default([]).describe("trusted addresses: address book, previous real payees") },
|
|
8
|
+
async ({ to, known }) => { try { const r = checkRecipient(to, known); return { content: [{ type: "text", text: `Verdict: ${r.verdict}. ${r.note}\n\n${JSON.stringify(r)}` }] }; } catch (e) { return { isError: true, content: [{ type: "text", text: String(e.message || e) }] }; } });
|
|
9
|
+
server.tool("onchain_check_info", "How to buy the hosted on-chain check that scans the sender's real transfer history (Ethereum, Base, Avalanche) for poisoning transfers and lookalikes: x402, 0.005 USDC on Base. Does not pay.", {},
|
|
10
|
+
async () => ({ content: [{ type: "text", text: `GET ${DEFAULT_BASE}/v1/check?from=<sending wallet>&to=<recipient> returns HTTP 402 (x402 v1 body + v2 PAYMENT-REQUIRED header), 5000 units USDC on Base = 0.005 USDC. Settled only after the report is produced. Docs: ${DEFAULT_BASE}/openapi.json` }] }));
|
|
11
|
+
return server;
|
|
12
|
+
}
|