yoinkai-mcp 0.1.0 → 0.2.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/README.md +3 -2
- package/SKILL.md +12 -3
- package/index.js +129 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# yoinkai-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MCP server over the [YOINK](https://yoinkai.fun) launch graph on Robinhood Chain. Give any agent eyes on token launches as they land — and let it prep a launch of your own in one command.
|
|
4
4
|
|
|
5
|
-
No keys. No custody.
|
|
5
|
+
No keys. No custody. This server never signs or sends anything: `launch_token` builds the exact unsigned transaction and hands it to you; you review and sign in your own wallet. Buys and sells happen in your own wallet on [yoinkai.fun](https://yoinkai.fun).
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -24,6 +24,7 @@ Cursor, Claude Desktop, or any MCP client:
|
|
|
24
24
|
- **scan_launch** / **xray_token** — safety X-ray of any token address: sellable, 0% tax, no owner control, LP locked, template-verified.
|
|
25
25
|
- **deployer_history** — every launch from a wallet, with a serial-deployer flag.
|
|
26
26
|
- **graph_stats** — totals for the launch graph: tokens indexed, unique deployers, top serial deployers.
|
|
27
|
+
- **launch_token** — build the unsigned transaction that launches a new token through the audited YoinkLaunch factory: fixed 1B supply, pool seeded in the same tx, liquidity locked forever, flat 0.0005 ETH fee, optional creator first buy. Returns `to`/`value`/`data` plus a plain-language review block; pass your wallet address as `from` and it dry-runs the exact tx against the chain before anyone signs. You sign in your own wallet — the server can't.
|
|
27
28
|
|
|
28
29
|
The graph indexes launches as they land — launchpad launches, factory deploys, and raw contract deploys.
|
|
29
30
|
|
package/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yoink
|
|
3
|
-
description: Watch
|
|
3
|
+
description: Watch, X-ray, and launch tokens on Robinhood Chain via the YOINK launch graph. Use when the user asks about new tokens, launches, token safety, deployer reputation, or wants to launch a token on Robinhood Chain.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# YOINK — launches on Robinhood Chain
|
|
7
7
|
|
|
8
|
-
You have the `yoink` MCP server (package `yoinkai-mcp`). It
|
|
8
|
+
You have the `yoink` MCP server (package `yoinkai-mcp`). It reads the YOINK launch graph and the chain, and it can BUILD an unsigned launch transaction — but it holds no keys and can never sign or send. All signing happens in the user's own wallet; trading happens at https://yoinkai.fun.
|
|
9
9
|
|
|
10
10
|
## What you can do
|
|
11
11
|
|
|
@@ -13,10 +13,19 @@ You have the `yoink` MCP server (package `yoinkai-mcp`). It is read-only: it rea
|
|
|
13
13
|
2. **X-ray a token** — `scan_launch` (alias `xray_token`) with a 0x address answers: is it sellable, is there tax, does an owner control it, is the LP locked, does the bytecode match the verified standard template. ALWAYS X-ray a token before saying anything positive about it.
|
|
14
14
|
3. **Check the deployer** — `deployer_history` with a wallet address shows every launch from that wallet and flags serial deployers. A deployer with many short-lived launches is a risk signal.
|
|
15
15
|
4. **Graph totals** — `graph_stats` for indexed-token counts, unique deployers, and the top serial deployers.
|
|
16
|
+
5. **Launch a token** — `launch_token` with a name and ticker builds the exact unsigned transaction for the audited YoinkLaunch factory: fixed 1B supply, pool created and seeded in the same tx, liquidity locked forever, flat 0.0005 ETH fee, optional creator first buy (`initialBuyEth`). If you know the user's wallet address, pass it as `from` so the tool dry-runs the exact tx on-chain before anyone signs.
|
|
17
|
+
|
|
18
|
+
## Launch flow (follow exactly)
|
|
19
|
+
|
|
20
|
+
1. Collect name + ticker (description, logo URI, links, first buy optional). Logo must already be an `ipfs://` or `https://` URI — image uploads happen on yoinkai.fun, not here.
|
|
21
|
+
2. Call `launch_token`. If it errors, relay the reason plainly; nothing was built.
|
|
22
|
+
3. SHOW THE USER THE REVIEW BLOCK before anything is signed: name, ticker, total ETH (fee + first buy), and that supply and liquidity terms are fixed by the factory. Never skip this.
|
|
23
|
+
4. The user signs the returned `to`/`value`/`data` on chain 4663 from their own wallet or signer. If they have no signer wired up, send them to https://yoinkai.fun to launch there instead.
|
|
24
|
+
5. Only claim the token is live after the transaction confirms — the token address is in the receipt, or `scan_launch` it once the graph indexes it. Never claim a launch happened because the tx was built.
|
|
16
25
|
|
|
17
26
|
## Rules
|
|
18
27
|
|
|
19
|
-
-
|
|
28
|
+
- Non-custodial: never claim you can sign, send, buy, or sell for the user. `launch_token` only PREPARES a transaction. When they want to trade, send them to the token's page on https://yoinkai.fun (append the 0x address to a token page link only if you have verified it via X-ray).
|
|
20
29
|
- Never invent metrics. Everything you state about a token must come from a tool result.
|
|
21
30
|
- An X-ray that does NOT match the template is a warning, not a verdict — say the contract needs manual verification before trading.
|
|
22
31
|
- Copycats exist: identical names or tickers on different contracts. The address is the identity; the name is not. When two tokens share a name, the older contract is usually the original.
|
package/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// yoinkai-mcp:
|
|
3
|
-
// Any agent (Claude, Cursor, Codex) can scan launches, X-ray tokens,
|
|
4
|
-
// history
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// yoinkai-mcp: MCP server over the YOINK launch graph on Robinhood Chain.
|
|
3
|
+
// Any agent (Claude, Cursor, Codex) can scan launches, X-ray tokens, read deployer
|
|
4
|
+
// history, and BUILD an unsigned token-launch transaction. Non-custodial by design:
|
|
5
|
+
// no keys, nothing is ever signed or sent by this server — the user reviews and signs
|
|
6
|
+
// every transaction in their own wallet. Zero deps, Node >= 18 (global fetch), speaks
|
|
7
|
+
// MCP stdio JSON-RPC 2.0.
|
|
7
8
|
|
|
8
9
|
const API = process.env.YOINK_API || 'https://yoink-graph.fly.dev';
|
|
9
10
|
const RPCS = [API + '/rpc', 'https://robinhoodchain.blockscout.com/api/eth-rpc', 'https://rpc.mainnet.chain.robinhood.com'];
|
|
@@ -77,6 +78,111 @@ async function xray(addr) {
|
|
|
77
78
|
};
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
// ---- launch verb: builds the tx, NEVER signs or sends (no keys in this process) ----
|
|
82
|
+
// Audited YoinkLaunch factory (docs/AUDIT_YOINKLAUNCH.md): fixed 1B supply, token is
|
|
83
|
+
// always token1, LP minted to an escape-proof locker, flat 0.0005 ETH fee. Encoder is
|
|
84
|
+
// a byte-exact port of the proven web path (landing/terminal.html encYoinkLaunch).
|
|
85
|
+
const FACTORY = '0xbcc67639cd2c3e9498714be53018949e3b17ea5e';
|
|
86
|
+
const LAUNCH_FEE = 500000000000000n; // 0.0005 ETH, compile-time constant in the factory
|
|
87
|
+
const SEL_LAUNCH = '0xe1f7d84a'; // launch((string,string,string,string,string[3],uint256))
|
|
88
|
+
const SEL_FEE = '0x09197a81'; // LAUNCH_FEE()
|
|
89
|
+
const CHAIN_ID = 4663; // 0x1237 Robinhood Chain
|
|
90
|
+
|
|
91
|
+
const w256 = (n) => BigInt(n).toString(16).padStart(64, '0');
|
|
92
|
+
const padStr = (s) => { const b = Array.from(new TextEncoder().encode(s)); const hx = b.map((x) => x.toString(16).padStart(2, '0')).join(''); return w256(b.length) + (b.length ? hx.padEnd(Math.ceil(hx.length / 64) * 64, '0') : ''); };
|
|
93
|
+
const tupDyn = (parts) => { let head = '', tail = '', off = parts.length * 32; for (const p of parts) { head += w256(off); tail += p; off += p.length / 2; } return head + tail; };
|
|
94
|
+
// launch(LaunchParams{name,symbol,logo,description,socials[3],minTokensOut})
|
|
95
|
+
function encYoinkLaunch(o, minOut) {
|
|
96
|
+
const soc = tupDyn([padStr(o.socials[0] || ''), padStr(o.socials[1] || ''), padStr(o.socials[2] || '')]);
|
|
97
|
+
const dyn = [padStr(o.name), padStr(o.symbol), padStr(o.logo || ''), padStr(o.desc || ''), soc];
|
|
98
|
+
let head = '', tail = '', off = 6 * 32;
|
|
99
|
+
for (const d of dyn) { head += w256(off); tail += d; off += d.length / 2; }
|
|
100
|
+
head += w256(minOut);
|
|
101
|
+
return SEL_LAUNCH + w256(0x20) + head + tail;
|
|
102
|
+
}
|
|
103
|
+
// expected tokens out for the creator initial buy on a FRESH pool (deterministic:
|
|
104
|
+
// single-sided position [-887200,204200], price starts at the upper tick).
|
|
105
|
+
// exact V3 math: L=amount1*Q96/(sqrtU-sqrtL); token0 in: sqrtP1=L*Q96*sqrtP0/(L*Q96+dx*sqrtP0)
|
|
106
|
+
function ylExpectedOut(buyWei) {
|
|
107
|
+
const Q96 = 79228162514264337593543950336n, sU = 2151813121295408910812139624586144n, sL = 4306310044n;
|
|
108
|
+
const L = (1000000000000000000000000000n * Q96) / (sU - sL);
|
|
109
|
+
const dx = buyWei * 9900n / 10000n; // 1% pool fee off the input
|
|
110
|
+
const sP1 = (L * Q96 * sU) / ((L * Q96) + (dx * sU));
|
|
111
|
+
return (L * (sU - sP1)) / Q96;
|
|
112
|
+
}
|
|
113
|
+
const fmtEth = (wei) => { const s = wei.toString().padStart(19, '0'); const i = s.slice(0, -18), f = s.slice(-18).replace(/0+$/, ''); return (i || '0') + (f ? '.' + f : ''); };
|
|
114
|
+
|
|
115
|
+
async function buildLaunch(args) {
|
|
116
|
+
const name = String(args.name || '').trim();
|
|
117
|
+
if (!name) throw new Error('need a token name');
|
|
118
|
+
if (name.length > 32) throw new Error('name too long (max 32 characters)');
|
|
119
|
+
const symbol = String(args.symbol || '').toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
120
|
+
if (!symbol) throw new Error('need a ticker symbol (letters and digits)');
|
|
121
|
+
if (symbol.length > 10) throw new Error('symbol too long (max 10 characters)');
|
|
122
|
+
const desc = String(args.description || '');
|
|
123
|
+
if (desc.length > 180) throw new Error('description too long (max 180 characters)');
|
|
124
|
+
const logo = String(args.logo || '');
|
|
125
|
+
if (logo && !/^(ipfs:\/\/|https:\/\/)/.test(logo)) throw new Error('logo must be an ipfs:// or https:// URI (upload the image on yoinkai.fun to get one)');
|
|
126
|
+
if (logo.length > 256) throw new Error('logo URI too long (max 256 characters)');
|
|
127
|
+
const socials = {};
|
|
128
|
+
for (const k of ['x', 'telegram', 'website']) {
|
|
129
|
+
const v = String(args[k] || '');
|
|
130
|
+
if (v && !/^https:\/\//.test(v)) throw new Error(k + ' link must start with https://');
|
|
131
|
+
if (v.length > 256) throw new Error(k + ' link too long (max 256 characters)');
|
|
132
|
+
socials[k] = v;
|
|
133
|
+
}
|
|
134
|
+
let buyWei = 0n;
|
|
135
|
+
if (args.initialBuyEth != null && args.initialBuyEth !== '') {
|
|
136
|
+
const n = parseFloat(args.initialBuyEth);
|
|
137
|
+
if (!isFinite(n) || n < 0) throw new Error('initialBuyEth must be a non-negative number of ETH');
|
|
138
|
+
if (n > 1) throw new Error('initialBuyEth over 1 ETH — set it lower, or launch on yoinkai.fun if you really mean it');
|
|
139
|
+
if (n > 0) buyWei = BigInt(Math.round(n * 1e18)); // same conversion as the web path
|
|
140
|
+
}
|
|
141
|
+
const minOut = buyWei > 0n ? ylExpectedOut(buyWei) * 9800n / 10000n : 0n; // 2% slippage, matches web
|
|
142
|
+
const data = encYoinkLaunch({ name, symbol, logo, desc, socials: [socials.x, socials.telegram, socials.website] }, minOut);
|
|
143
|
+
const value = LAUNCH_FEE + buyWei;
|
|
144
|
+
const valueHex = '0x' + value.toString(16);
|
|
145
|
+
|
|
146
|
+
// tripwire: the deployed factory must still charge exactly the audited fee.
|
|
147
|
+
// A malformed/empty return (e.g. '0x') counts as unverified, not as a mismatch.
|
|
148
|
+
let feeVerified = false;
|
|
149
|
+
const feeHex = await rpc('eth_call', [{ to: FACTORY, data: SEL_FEE }, 'latest']).catch(() => null);
|
|
150
|
+
if (typeof feeHex === 'string' && /^0x[0-9a-fA-F]{64}$/.test(feeHex)) {
|
|
151
|
+
if (BigInt(feeHex) !== LAUNCH_FEE) throw new Error('on-chain LAUNCH_FEE does not match the audited constant — refusing to build. Report this.');
|
|
152
|
+
feeVerified = true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// optional dry run from the user's wallet (needs their address, not their keys)
|
|
156
|
+
let simulation = { ran: false, note: 'pass your wallet address as "from" to dry-run the exact tx before signing' };
|
|
157
|
+
if (args.from != null && args.from !== '') {
|
|
158
|
+
if (!isAddr(args.from)) throw new Error('"from" must be a 0x wallet address');
|
|
159
|
+
const from = args.from.toLowerCase();
|
|
160
|
+
try {
|
|
161
|
+
const pre = await rpc('eth_call', [{ from, to: FACTORY, value: valueHex, data }, 'latest']);
|
|
162
|
+
if (pre && pre.length >= 130) simulation = { ran: true, ok: true, predictedToken: '0x' + pre.slice(26, 66), predictedPool: '0x' + pre.slice(90, 130) };
|
|
163
|
+
else throw new Error('empty result');
|
|
164
|
+
} catch (e) {
|
|
165
|
+
throw new Error('simulation reverted — this launch would fail on-chain, nothing to sign. Check the wallet holds fee + buy + gas (~0.002 ETH headroom) on Robinhood Chain and retry. (' + String(e.message || e) + ')');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
action: 'REVIEW_THEN_SIGN',
|
|
171
|
+
network: { chainId: CHAIN_ID, chainIdHex: '0x1237', name: 'Robinhood Chain', explorer: 'https://robinhoodchain.blockscout.com', rpc: 'https://rpc.mainnet.chain.robinhood.com' },
|
|
172
|
+
tx: { to: FACTORY, value: valueHex, data },
|
|
173
|
+
cost: { launchFeeEth: fmtEth(LAUNCH_FEE), initialBuyEth: fmtEth(buyWei), totalValueEth: fmtEth(value), gasNote: 'keep ~0.002 ETH extra for gas' },
|
|
174
|
+
review: {
|
|
175
|
+
name, symbol, description: desc || null, logo: logo || null,
|
|
176
|
+
x: socials.x || null, telegram: socials.telegram || null, website: socials.website || null,
|
|
177
|
+
supply: '1,000,000,000 (fixed by the factory, 18 decimals)',
|
|
178
|
+
initialBuy: buyWei > 0n ? fmtEth(buyWei) + ' ETH spent as a first buy to the creator, min tokens out ' + minOut.toString() + ' (fresh-pool quote, 2% slippage)' : 'none',
|
|
179
|
+
factory: FACTORY, feeVerifiedOnChain: feeVerified, simulation,
|
|
180
|
+
},
|
|
181
|
+
launchDesign: 'Audited YoinkLaunch factory: token deploys, pool is created and seeded with the full supply in the same tx, and the LP position is locked in a contract with no withdraw path — liquidity can never be pulled.',
|
|
182
|
+
sign: 'This server holds no keys and CANNOT send this transaction. Show the review to the user, then have them sign exactly this tx (to/value/data on chain 4663) from their own wallet or agent signer — or launch by hand at https://yoinkai.fun. The new token address is in the receipt (or scan_launch it once the graph picks it up).',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
80
186
|
const TOOLS = [
|
|
81
187
|
{ name: 'top_launches', description: 'Most active recent token launches on Robinhood Chain by newest block. Returns name, symbol, address, deployer, source.',
|
|
82
188
|
inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'how many, default 20, max 100' } } } },
|
|
@@ -88,6 +194,18 @@ const TOOLS = [
|
|
|
88
194
|
inputSchema: { type: 'object', properties: { address: { type: 'string', description: '0x deployer wallet' } }, required: ['address'] } },
|
|
89
195
|
{ name: 'graph_stats', description: 'Totals for the YOINK launch graph: tokens indexed, unique deployers, top serial deployers.',
|
|
90
196
|
inputSchema: { type: 'object', properties: {} } },
|
|
197
|
+
{ name: 'launch_token', description: 'Build the UNSIGNED transaction that launches a new token on Robinhood Chain via the audited YoinkLaunch factory (fixed 1B supply, liquidity locked forever, 0.0005 ETH flat fee, optional creator first buy). Returns to/value/data for the user to review and sign in their OWN wallet — this server never holds keys and cannot send anything. ALWAYS show the review block to the user before they sign.',
|
|
198
|
+
inputSchema: { type: 'object', properties: {
|
|
199
|
+
name: { type: 'string', description: 'token name, max 32 chars' },
|
|
200
|
+
symbol: { type: 'string', description: 'ticker, letters/digits, max 10 chars' },
|
|
201
|
+
description: { type: 'string', description: 'optional, max 180 chars, stored on-chain in the launch event' },
|
|
202
|
+
logo: { type: 'string', description: 'optional ipfs:// or https:// image URI' },
|
|
203
|
+
x: { type: 'string', description: 'optional https:// X/Twitter link' },
|
|
204
|
+
telegram: { type: 'string', description: 'optional https:// Telegram link' },
|
|
205
|
+
website: { type: 'string', description: 'optional https:// website link' },
|
|
206
|
+
initialBuyEth: { type: 'number', description: 'optional creator first buy in ETH, same tx (max 1)' },
|
|
207
|
+
from: { type: 'string', description: 'optional 0x wallet address that will sign — enables an exact dry run against the chain before anyone signs' },
|
|
208
|
+
}, required: ['name', 'symbol'] } },
|
|
91
209
|
];
|
|
92
210
|
|
|
93
211
|
async function call(name, args) {
|
|
@@ -109,9 +227,14 @@ async function call(name, args) {
|
|
|
109
227
|
launches: launches.map((t) => ({ name: t.name, symbol: t.symbol, address: t.address, block: t.block })) };
|
|
110
228
|
}
|
|
111
229
|
if (name === 'graph_stats') return j('/v0/stats');
|
|
230
|
+
if (name === 'launch_token') return buildLaunch(args);
|
|
112
231
|
throw new Error('unknown tool ' + name);
|
|
113
232
|
}
|
|
114
233
|
|
|
234
|
+
// exported for the audit/parity harness; running as a bin starts the stdio server below
|
|
235
|
+
module.exports = { encYoinkLaunch, ylExpectedOut, buildLaunch, call, TOOLS, FACTORY, LAUNCH_FEE };
|
|
236
|
+
if (require.main !== module) return;
|
|
237
|
+
|
|
115
238
|
// ---- MCP stdio transport ----
|
|
116
239
|
const send = (m) => process.stdout.write(JSON.stringify(m) + '\n');
|
|
117
240
|
let buf = '';
|
|
@@ -125,7 +248,7 @@ process.stdin.on('data', async (d) => {
|
|
|
125
248
|
const reply = (result) => send({ jsonrpc: '2.0', id: msg.id, result });
|
|
126
249
|
const fail = (message) => send({ jsonrpc: '2.0', id: msg.id, error: { code: -32000, message } });
|
|
127
250
|
try {
|
|
128
|
-
if (msg.method === 'initialize') reply({ protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'yoinkai-mcp', version: '0.
|
|
251
|
+
if (msg.method === 'initialize') reply({ protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'yoinkai-mcp', version: '0.2.0' } });
|
|
129
252
|
else if (msg.method === 'tools/list') reply({ tools: TOOLS });
|
|
130
253
|
else if (msg.method === 'tools/call') {
|
|
131
254
|
const out = await call(msg.params.name, msg.params.arguments);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yoinkai-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server over the YOINK launch graph on Robinhood Chain: scan launches, X-ray tokens for safety, read deployer history, and build unsigned token-launch transactions you sign in your own wallet. No keys, no custody.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": { "yoinkai-mcp": "index.js" },
|
|
7
7
|
"files": ["index.js", "README.md", "SKILL.md"],
|