animotion-mcp appeared on the npm registry on Apr 5, 2026, published under the GitHub org animotion-mcp. The package registers as an MCP server providing CSS animation utilities and icon search for AI coding assistants. The npm package ships clean: server code, icon data, and a utilities stylesheet. The repository at github[.]com/animotion-mcp/animotion-mcp.github.io carries the payload. analytics/api/mcp-events.js, a Vercel serverless analytics handler, contains a ~29,446-byte obfuscated JavaScript loader appended after 507 characters of whitespace on line 393. The loader queries public Ethereum RPC endpoints to resolve C2 addresses from a hardcoded sender wallet, downloads XOR-encrypted second-stage payloads, and executes them both in-process via eval() and as a detached background process. The campaign distributed the project across three awesome-mcp-servers lists, five MCP directories, Dev.to, and Twitter/X on the same day.
The artifact
The npm package animotion-mcp (v1.0.2 through v1.0.3) lists @modelcontextprotocol/sdk, lucide-static, heroicons, @tabler/icons, and bootstrap-icons as dependencies. The server in mcp/server.js (641 lines) exposes 10 tools (six animation queries, four icon searches) and four resources over stdio transport. All tools are read-only queries against in-memory data. No filesystem writes, no shell execution, no dynamic code evaluation.
The files array in package.json restricts the published package to server.js, icon-loader.js, api.json, icons-data.js, and utilities.css. The analytics/ directory is excluded. The npm package as published does not carry the dropper.
The repository contains a 930-line README with framework integration examples, browser compatibility charts, and marketing copy targeting developers who use Claude Code, Cursor, and Windsurf. CODE_OF_CONDUCT.md, CONTRIBUTING.md, a fabricated CHANGELOG claiming releases with no corresponding commits, a Google Search Console verification file, and GA4 analytics fill out the trust padding. Two session logs in ops/ document the entire project being built in two afternoon sessions with an AI coding assistant. The git history is a single squashed commit dated Apr 12, 2026.
What it does
Lines 1 through 392 of analytics/api/mcp-events.js contain a legitimate Vercel serverless function: an Upstash Redis-backed analytics handler that records MCP server telemetry. The closing brace sits at the start of line 393. After that brace, 507 whitespace characters push a ~29,446-byte obfuscated payload off-screen on the same line. The obfuscation uses a standard JavaScript obfuscator: _0x-prefixed variable names and a 297-element string rotation array.
The deobfuscated payload imports node:http, node:https, node:zlib, node:url, and spawn from node:child_process. It sets a campaign identifier:
global.i = "A9-4051-1";
global.r = require;
typeof module === "object" && (global.m = module);
The loader constructs C2 addresses from the Ethereum blockchain rather than from a hardcoded domain or IP. Five public endpoints serve as the lookup infrastructure:
| Endpoint | Role |
|---|---|
1rpc[.]io/eth | Ethereum RPC |
ethereum-rpc[.]publicnode[.]com | Ethereum RPC |
eth-mainnet[.]public[.]blastapi[.]io | Ethereum RPC |
eth[.]drpc[.]org | Ethereum RPC |
eth[.]blockscout[.]com/api | Indexer API fallback |
The wallet address 0xa322e5f3d311d3080e9adc2490ef6f0121063e1a is stored in the obfuscated string table. The loader races requests across the endpoints to find the latest transaction from this wallet using a binary search via eth_getBlockByNumber and eth_getTransactionCount. The to field of the matching transaction encodes two IPv4 addresses: bytes 0 through 3 become IP1, bytes 4 through 7 become IP2. The adversary rotates C2 servers by sending a new Ethereum transaction from the same wallet to a new address.
Two payloads download from the resolved IP:
| Path | XOR key | Execution |
|---|---|---|
/0x/ls | @^1aQk,Sr3=@ | Detached child process |
/0x/cl | q4FZkxX{!hy-p_>d$0B& | eval() in-process + detached child |
If the response body is empty, the loader reads the payload from a custom x-payload-B6 response header, base64-decodes it, and applies the same XOR. The User-Agent is set to Chrome 131 on Windows 10. A Sec-V header carries the campaign identifier.
spawn("node", ["-e", payload], {
detached: true,
stdio: "ignore",
windowsHide: true
}).unref();
The detached child receives require and module through global assignments, giving it full Node.js capability. It survives parent exit and does not appear in the parent’s stdio.
config.bat, listed in .gitignore but present in the cloned repository, is a Windows batch script that changes the system clock to a prior commit timestamp, amends the commit with --no-verify, and force-pushes. This explains the single-commit history and the mismatch between CHANGELOG dates and git history.
The mcp/server.js ping() function sends tool name, version, and outcome to process.env.ANIMOTION_ANALYTICS_URL. Telemetry is disabled by default. The intended destination is the Vercel deployment of the analytics handler, which executes the blockchain C2 dropper on every cold start.
The campaign
The operator documented 46 distribution actions across roughly 3.5 hours on Apr 5, 2026 in ops/SESSION_LOG_2026-04-05.md. The npm package was published with OTP. Pull requests went to three awesome-mcp-servers lists (punkpeye at 84K stars, appcypher at 5.4K, wong2 at 3.9K). Issues were filed on chatmcp/mcpso (#1512) and cline/mcp-marketplace (#1248). Submissions went to Glama, Smithery, PulseMCP, mcpservers[.]org, and Cursor Directory. A Dev.to article and Twitter/X post went out the same day. A prior GitHub org (animationiconsmcp) was deleted during the launch. The original repository name css3-animations surfaced in generate-api.js, indicating a rebrand before distribution.
The blockchain C2 mechanism, the campaign identifier prefix (A9-), the /0x/ C2 path convention, the XOR decryption pattern, and the dual eval()/spawn() execution path match the infrastructure in the agentgui analysis (campaign A9-2057, wallet 0xa322e5f3d311d3080e6f0121063e9adc2490ef1a). The two wallet addresses share the same first 10 bytes and last byte, with the intervening 10 bytes composed of the same two five-byte sequences in transposed order.
Why the operation matters here
The npm package ships clean. npm audit, lockfile review, and ignore-scripts=true collectively miss the payload. The dropper lives in the repository and in the Vercel serverless deployment at animotion-chi[.]vercel[.]app. A developer who clones the repository to contribute, inspect, or self-host the server inherits it. The Vercel function executes the dropper on every cold start.
The GitHub Pages workflow (.github/workflows/pages.yml) sets path: '.', deploying the entire repository root. The obfuscated payload, config.bat, and the session logs are all publicly served at the Pages URL.
What a defender can do
Search for animotion-mcp in MCP configuration files (mcp.json, .cursor/mcp.json, claude_desktop_config.json). The npm package does not contain the dropper, but any local clone of the repository does. Check analytics/api/mcp-events.js line 393 for content beyond the closing brace: the payload begins after 507 whitespace characters.
Monitor Ethereum wallet 0xa322e5f3d311d3080e9adc2490ef6f0121063e1a for outbound transactions. Each transaction updates the C2 addresses the loader resolves.
If the Vercel deployment at animotion-chi[.]vercel[.]app received traffic, any serverless function invocation that loaded the analytics handler executed the dropper.
Where Aephix fits
The npm package passes every automated check. The payload sits in a repository file that no registry scanner inspects. Before you install a package or connect to a server, Aephix Vantage gives you a free, cross-ecosystem view of what is already known to be malicious, so a component with a hostile history is something you recognize before you connect. Every week, Weekly Sleuth links the malicious packages, models, skills, MCP servers, extensions, and containers confirmed that week to the wider operations behind them, with a confidence level and supporting evidence, so subscribers act against the whole operation rather than the single artifact.
Indicators of compromise
| Type | Indicator | Context |
|---|---|---|
| npm package | animotion-mcp (v1.0.2 through v1.0.3) | Clean npm package, payload excluded via files array |
| Repository | github[.]com/animotion-mcp/animotion-mcp.github.io | Source repository with malicious analytics handler |
| Payload file | analytics/api/mcp-events.js line 393 | ~29,446-byte obfuscated loader after 507 whitespace chars |
| Campaign ID | A9-4051-1 | Set as global.i, sent in Sec-V header |
| Ethereum wallet | 0xa322e5f3d311d3080e9adc2490ef6f0121063e1a | Sender address, transactions encode C2 IPv4 addresses |
| C2 path | /0x/ls | XOR-decrypted, detached child process |
| C2 path | /0x/cl | XOR-decrypted, eval() + detached child |
| XOR key | @^1aQk,Sr3=@ | Decryption key for /0x/ls payload |
| XOR key | q4FZkxX{!hy-p_>d$0B& | Decryption key for /0x/cl payload |
| HTTP header | x-payload-B6 | Fallback payload delivery via response header |
| RPC endpoint | 1rpc[.]io/eth | Ethereum RPC for C2 resolution |
| RPC endpoint | ethereum-rpc[.]publicnode[.]com | Ethereum RPC for C2 resolution |
| RPC endpoint | eth-mainnet[.]public[.]blastapi[.]io | Ethereum RPC for C2 resolution |
| RPC endpoint | eth[.]drpc[.]org | Ethereum RPC for C2 resolution |
| API endpoint | eth[.]blockscout[.]com/api | Indexer fallback for transaction history |
| Domain | animotion-mcp[.]github[.]io | GitHub Pages site |
| Domain | animotion-chi[.]vercel[.]app | Vercel deployment hosting compromised analytics |
| Domain | bachao[.]ai | Publisher organization domain |
| ceo[@]bachao[.]ai | Git commit author, Glama submission | |
| GA4 ID | G-EXLSDRKQWR | Google Analytics on the Pages site |
| Google verification | googlef9113ea9882ba9d2 | Search Console verification file |
| Prior repository | github[.]com/nickshouvik/css3-animations | Original identity before rebrand |
| Deleted GitHub org | animationiconsmcp | Prior GitHub org, deleted during launch |
| GitHub account | ceo-shouvik | Commit author, npm publisher |
| Twitter/X | @CEOShouvik | Launch announcement account |