hookit-mcp 0.7.2 → 0.8.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.
Files changed (3) hide show
  1. package/README.md +6 -5
  2. package/index.js +384 -103
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,7 +18,7 @@ Lets an AI agent launch a coin on [hookit.fun](https://www.hookit.fun) (Ink) by
18
18
 
19
19
  The key signs on this machine and is never sent anywhere. Give it a wallet funded for what the agent should do and nothing else.
20
20
 
21
- Optional env: `HOOKIT_API_URL` (default `https://www.hookit.fun`), `HOOKIT_RPC_URL` (default Ink public RPC), `HOOKIT_MAX_BUY_ETH` (largest single `buy_token` paid in ETH, default `0.5`), `HOOKIT_MAX_BUY_USD` (largest single `buy_token` paid in a token, in dollars, default `1000`), `HOOKIT_SEND_ETH_TO` (comma-separated addresses `send_eth` may pay, for example your Kraken deposit address on Ink; unset, any address), `HOOKIT_MEMORY_EVOLVE` / `HOOKIT_MEMORY_LENS` / `HOOKIT_MEMORY_FEE1` / `HOOKIT_MEMORY_FEE2` (override Memory on Evolve's pinned addresses), `HOOKIT_VELOCITY_FEE_PLUGIN` / `HOOKIT_VELOCITY_EVOLVE` / `HOOKIT_VELOCITY_EVOLVE_FEE1` / `HOOKIT_VELOCITY_EVOLVE_FEE2` (override Velocity Fee's pinned addresses), `HOOKIT_MASTER_HOOK_D` (overrides the pinned hook D, the Master hook new launches go to; coins keep trading on their own PoolKey's hook either way, this only names it in `coin_info`).
21
+ Optional env: `HOOKIT_API_URL` (default `https://www.hookit.fun`), `HOOKIT_RPC_URL` (default Ink public RPC), `HOOKIT_RELAY_API_URL` (default `https://api.relay.link`) and `HOOKIT_RELAY_API_KEY` (a Relay API key, for higher quote limits), `HOOKIT_MAX_BUY_ETH` (largest single `buy_token` paid in ETH, default `0.5`), `HOOKIT_MAX_BUY_USD` (largest single `buy_token` paid in a token, in dollars, default `1000`), `HOOKIT_SEND_ETH_TO` (comma-separated addresses `send_eth` may pay, for example your Kraken deposit address on Ink; unset, any address), `HOOKIT_MEMORY_EVOLVE` / `HOOKIT_MEMORY_LENS` / `HOOKIT_MEMORY_FEE1` / `HOOKIT_MEMORY_FEE2` (override Memory on Evolve's pinned addresses), `HOOKIT_VELOCITY_FEE_PLUGIN` / `HOOKIT_VELOCITY_EVOLVE` / `HOOKIT_VELOCITY_EVOLVE_FEE1` / `HOOKIT_VELOCITY_EVOLVE_FEE2` (override Velocity Fee's pinned addresses), `HOOKIT_MASTER_HOOK_D` (overrides the pinned hook D, the Master hook new launches go to; coins keep trading on their own PoolKey's hook either way, this only names it in `coin_info`). `HOOKIT_EVOLVE_PLUGIN` (overrides the pinned Evolve v2 plugin, where new Evolve coins launch) and `HOOKIT_EVOLVE_V2_MEMORY_EVOLVE` / `_MEMORY_LENS` / `_MEMORY_FEE1` / `_MEMORY_FEE2` / `_VELOCITY_EVOLVE` / `_VELOCITY_EVOLVE_FEE1` / `_VELOCITY_EVOLVE_FEE2` (override Memory's and Velocity's Evolve v2 deployments; the `HOOKIT_MEMORY_*` and `HOOKIT_VELOCITY_EVOLVE*` ones are Evolve v1's).
22
22
 
23
23
  Coin names, tickers and descriptions are written by whoever launched the coin. The server hands them to the agent as untrusted data: `coin_info` withholds a description that carries a link, a handle, an address or a tool name, names and tickers that read as instructions are replaced, and every tool that signs is annotated so your MCP client can ask you before each call. Keep that confirmation on for a funded wallet, and set `HOOKIT_SEND_ETH_TO` if the agent only ever cashes out to one place.
24
24
 
@@ -65,15 +65,15 @@ Paying or receiving a token approves exactly that amount to the contract that tr
65
65
 
66
66
  ### Paying with something the coin does not trade in (Relay)
67
67
 
68
- `coin_info` lists what a coin trades in (`tradeIn`). When `payWith` or `receive` is not one of them, the server swaps through [Relay](https://relay.link) on Ink, the same route hookit.fun's own swap box uses: it asks `https://www.hookit.fun/api/relay/quote` for the swap (Relay's API key stays on that server), then signs Relay's transactions itself. A buy swaps first and buys with what Relay returned; a sale sells for the coin's pair first, then swaps the proceeds. `dryRun` shows both legs.
68
+ `coin_info` lists what a coin trades in (`tradeIn`). When `payWith` or `receive` is not one of them, the server swaps through [Relay](https://relay.link) on Ink, the same route hookit.fun's own swap box uses: it asks Relay's API (`https://api.relay.link`) for the swap directly, so hookit.fun never shapes the calls the wallet signs, then signs Relay's transactions itself. A buy swaps first and buys with what Relay returned; a sale sells for the coin's pair first, then swaps the proceeds. `dryRun` shows both legs.
69
69
 
70
- Before signing, Relay's transactions are checked twice. First their shape: Ink only, calls only to Relay's Ink contracts (router, approval proxy, receiver, multicaller, depository), an approval only of Relay's approval proxy (replaced by an exact approval of the amount), and no more ETH than the amount. Then their effect: the approval and every call run together in one `eth_simulateV1` from the wallet, and the swap is refused unless the wallet would give at most the amount, get at least the minimum, and lose nothing else. That minimum is never the quote's word alone: it is at least the market price (Chainlink's ETH/USD as Factory C reads it, a stock's USDG pool on Ink, else hookit.fun's price) less the slippage and 5% for Relay's fees and price impact, and a quote under that floor is refused. Anything else is refused and nothing is sent. No allowance to Relay is left behind, even when a swap fails. One Relay swap converts at most the per-buy cap (`HOOKIT_MAX_BUY_ETH` / `HOOKIT_MAX_BUY_USD`), with at most 3% slippage. Relay swaps ETH or USDG with each other, kBTC and the tokenized stocks. The RPC in `HOOKIT_RPC_URL` must support `eth_simulateV1` (Ink's public RPC does).
70
+ Before signing, Relay's transactions are checked twice. First their shape: Ink only, calls only to Relay's Ink contracts (router, approval proxy, receiver, multicaller, depository), an approval only of Relay's approval proxy (replaced by an exact approval of the amount), and no more ETH than the amount. Then their effect: the approval and every call run together in one validated `eth_simulateV1` from the wallet, priced with the same fee fields the swap is then signed with (so a called contract cannot tell the simulation by a zero gas price), and the swap is refused unless the wallet would give at most the amount, get at least the minimum, and lose nothing else. That minimum is never the quote's word alone: it is at least the market price (Chainlink's ETH/USD as Factory C reads it, a stock's USDG pool on Ink, else hookit.fun's price) less the slippage and 5% for Relay's fees and price impact, and a quote under that floor is refused. Anything else is refused and nothing is sent. No allowance to Relay is left behind, even when a swap fails. One Relay swap converts at most the per-buy cap (`HOOKIT_MAX_BUY_ETH` / `HOOKIT_MAX_BUY_USD`), with at most 3% slippage. Relay swaps ETH or USDG with each other, kBTC and the tokenized stocks. The RPC in `HOOKIT_RPC_URL` must support `eth_simulateV1` (Ink's public RPC does).
71
71
 
72
72
  When the first leg went through and the second did not, the reply is `partial: true` with what the wallet now holds: do not retry the call, or the first leg runs twice.
73
73
 
74
74
  ### What the server trusts
75
75
 
76
- The contracts it approves, pays or quotes from (Factory C and the Master factories, CreatorPayoutRouterC, HookitSwapRouter, the aggregator's RouteConfigC, Uniswap's V4Quoter, USDG, FloorLoanPlugin, the Gated Buy plugin, the Evolve plugin, NFT Strategy and Memory on Evolve, ModuleRegistry, Velocity Fee and Velocity on Evolve, and the Agent Pass) are pinned in the server, not read from hookit.fun: a Hookit redeploy needs a new hookit-mcp release. Since 0.7.2 the payout router is the M-01 CreatorPayoutRouterC `0x3e8E8a1F3D2279cB59042B029637c7096EeC2302` and `coin_info` names hook D `0x8a29B2c508007707bbFF1814386AA968bbeCbFff` (HookRegistry's default for new launches since 2026-10-16) next to hook C `0x4b533C432516FC1Be4b0bAEFe6BFF0942AD87FfF` (earlier coins). A launch prepared by hookit.fun is only signed when it goes to Factory C or its payout router with at most the launch fee plus `HOOKIT_MAX_BUY_ETH`. Tools that sign run one at a time, even when an agent calls several at once.
76
+ The contracts it approves, pays or quotes from (Factory C and the Master factories, CreatorPayoutRouterC, HookitSwapRouter, the aggregator's RouteConfigC, Uniswap's V4Quoter, USDG, FloorLoanPlugin, the Gated Buy plugin, the Evolve plugin, NFT Strategy and Memory on Evolve, ModuleRegistry, Velocity Fee and Velocity on Evolve, and the Agent Pass) are pinned in the server, not read from hookit.fun: a Hookit redeploy needs a new hookit-mcp release. Since 0.7.2 the payout router is the M-01 CreatorPayoutRouterC `0x3e8E8a1F3D2279cB59042B029637c7096EeC2302` and `coin_info` names hook D `0x8a29B2c508007707bbFF1814386AA968bbeCbFff` (HookRegistry's default for new launches since 2026-10-16) next to hook C `0x4b533C432516FC1Be4b0bAEFe6BFF0942AD87FfF` (earlier coins). A launch prepared by hookit.fun is only signed when it goes to Factory C or its payout router with at most the launch fee plus `HOOKIT_MAX_BUY_ETH`, and when its calldata pays the creator fees to the `payout` asked: this wallet through Factory C, or the router's provider, account id and wallet. A TikTok handle is hashed by the server itself and a GitHub handle looked up on api.github.com before preparing; an X handle without `accountId` takes hookit.fun's lookup, and the result's `payout` shows the account id the transaction pays. Tools that sign run one at a time, even when an agent calls several at once.
77
77
 
78
78
  ## Evolve: the wallet that launches decides
79
79
 
@@ -89,6 +89,7 @@ evolve_decide({ "token": "0x…", "feePct": 2, "reason": "Heavy selling: a highe
89
89
  - A setting that raises neither the fee nor the creator share applies at once. A raise is announced on the coin page and applies after the launch's notice (1 h); a new decision replaces it, `cancel: true` drops it.
90
90
  - The reason is optional and public on chain: plain text, no links, e-mails, domains or @handles (refused before sending).
91
91
  - The decider cannot change the code, the liquidity or the name, and cannot redirect money: the caps and the sinks are fixed at launch. A lost key leaves the coin on its live setting.
92
+ - Two Evolve versions (0.8.0): new Evolve coins launch on Evolve v2, the current module, read from the ModuleRegistry by the pinned v2 plugin's address (Evolve v1, module 5, is deprecated: no new launch on it). Coins launched on v1 keep working: every `evolve_*`, `memory_*` and `velocity_*` tool finds the plugin that knows the coin and uses that version's catalog and hooks. Until v2 has code at its address, the server launches on v1. A launch is signed only when its Evolve slot is the current module with exactly the 288 bytes of its settings: hookit-mcp never plugs hooks at launch (it plugs them with `evolve_plug`, `memory_set` and `velocity_set`). NFT Strategy is on Evolve v1 coins only for now.
92
93
 
93
94
  ## Evolve: hooks added later
94
95
 
@@ -211,7 +212,7 @@ launch_token({ "name": "…", "symbol": "…", "hookTaxPct": 3, "builderHooks":
211
212
 
212
213
  - `deploy_hook` takes the Foundry artifact (`out/<File>.sol/<Contract>.json` after `forge build`, as an absolute path: the server runs in its own folder) and refuses anything that is not a deployable plugin: built on the SDK's `HookPluginBase`, one `address` constructor, no linked libraries, code within Ink's limits. It reads regular files only, up to 16 MiB, and never quotes a file it cannot use. The constructor argument is always the ModuleRegistry pinned here, never one the agent or a website names.
213
214
  - `submit_hook` sends the manifest as the SDK wrote it and cleans the card exactly as the Workshop's form does, then signs the Workshop's submit message. Before signing, it checks that the manifest's codehash is the code deployed at its address and refuses a submission hookit.fun would refuse (a live plugin, or one that already has a submission in review or approved); it warns when the explorer names another deployer.
214
- - Nothing here is an audit: Hookit's review may ask for changes (a change is a new deployment and a new submission), and it can halt a plugin at any time.
215
+ - Hookit's review may ask for changes (a change is a new deployment and a new submission), and it can halt a plugin at any time.
215
216
  - Both signing tools are dry runs unless `dryRun` is `false`: deploy or submit only once the user said to.
216
217
 
217
218
  ## Backed Floor: redeem and borrow
package/index.js CHANGED
@@ -44,7 +44,7 @@ const API = (process.env.HOOKIT_API_URL || "https://www.hookit.fun").replace(/\/
44
44
  const INK_PUBLIC_RPC = "https://rpc-gel.inkonchain.com";
45
45
  const RPC = process.env.HOOKIT_RPC_URL || INK_PUBLIC_RPC;
46
46
  const KEY = process.env.HOOKIT_PRIVATE_KEY?.trim();
47
- const VERSION = "0.7.2";
47
+ const VERSION = "0.8.0";
48
48
  const EXPLORER = "https://explorer.inkonchain.com";
49
49
  /**
50
50
  * The Ink deployments this server signs against. Pinned here, never taken from hookit.fun's catalog: a compromised
@@ -74,7 +74,12 @@ const PINNED = {
74
74
  aggregator: "0x6518b4b2D77F8351F8cE7C9BA1ca7E6D8631D210", // BalancedAggregatorC (USDG in / out on v2 coins)
75
75
  loanPlugin: "0xA9e9347175B1d128478D0DdA475dE5a11C33DBB4", // FloorLoanPlugin
76
76
  gatedBuyPlugin: "0x4af18ddf7Ccbb0d30DE397B8ebdc873f98BFA9f3", // GatedBuyPlugin
77
- evolvePlugin: "0xC78B0195Fa8b3DeECF5f6CCe1F6F2A7902588112", // EvolvePlugin (v2 module 5; creates EvolveExtensions 0x0ef655FEC85BF298a8154c8A2f4DA5CB4Dd4aBcE), tx list c-evolve-57073-58022489
77
+ // ---- Evolve (spec/v2/plugins/evolve.md): v2 (§10.6, launch plugs and relays) takes every new launch; v1 keeps the
78
+ // coins launched on it (deprecated in the ModuleRegistry by the last v2 list, c-evolve-v1-deprecate: no new launch,
79
+ // its coins keep running). Each coin's tools use the plugin that knows it (`evolvePluginOf`). Until v2 has code at
80
+ // its address, the current Evolve is v1. HOOKIT_EVOLVE_PLUGIN overrides the v2 address.
81
+ evolvePlugin: "0x9E648Cbb0cf76197056aE5f61fdD9b6b95cdA1c4", // EvolvePlugin v2 (CreateX label hookit.v2.c1:c.EvolvePlugin.v2, list c-evolve-57073-58845088: module 28, EvolveExtensions 0x57c574e2764040cb1e325E591bC3475b3c035D75, relays 0x37292dEBc156Aa3Df8aD5311d1F7b41eA5198e28 / 0x3D855467718c16F106956C740E2e77eD70798D6A as modules 29 / 30); the module id is read from the ModuleRegistry
82
+ evolvePluginV1: "0xC78B0195Fa8b3DeECF5f6CCe1F6F2A7902588112", // EvolvePlugin v1 (module 5; creates EvolveExtensions 0x0ef655FEC85BF298a8154c8A2f4DA5CB4Dd4aBcE), tx list c-evolve-57073-58022489
78
83
  // NftStrategyEvolve f3 (KNOWN-ISSUES R9a, list c-evolve-nft-strategy-f3): Evolve hook id 8, raffle
79
84
  // 0x49eF571af836DD777C223Bcf90Fc5A4F8441B074. The first version 0x5C731E518dB36a542DF9907DEb37967a39dAAF0d (id 1) is
80
85
  // halted and was never plugged by any coin (no Announced on EvolveExtensions, 0 raffles), so nothing reads it any more.
@@ -104,6 +109,19 @@ const PINNED = {
104
109
  velocityEvolveFee2: "0x48C6b65554cB87cB04855426f7955bD355Ba6209", // VelocityEvolveFee rank 2 (asks the 3rd percent)
105
110
  agentPass: "0xe847CCD9EcD9889c39B381C83B33e0251759Cf67", // HookitAgentPass
106
111
  agentValidator: "0x7A9365784b438D327D468b717dd644fd0fB09fa7", // HookitAgentValidator (ERC-8004)
112
+ // ---- The Evolve v2 deployments of Memory and Velocity on Evolve (lists c-evolve-memory-57073-58845088, catalog ids
113
+ // 5-7, and c-velocity-evolve-57073-58845088, ids 0-2, against the v2 catalog): the memory* / velocityEvolve* pins above are v1's and serve the v1 coins. HOOKIT_EVOLVE_V2_MEMORY_EVOLVE,
114
+ // _MEMORY_LENS, _MEMORY_FEE1, _MEMORY_FEE2, _VELOCITY_EVOLVE, _VELOCITY_EVOLVE_FEE1, _VELOCITY_EVOLVE_FEE2 override them;
115
+ // nothing is used until code is there and the contracts point at the v2 plugin and catalog.
116
+ evolveV2: {
117
+ memoryEvolve: "0x68B0dE60217DC4Ca971eDc23B0c9B4B6D3A710A1",
118
+ memoryLens: "0xD33EAe11B39c6348e6e803Ee8a43176AeD727924",
119
+ memoryFee1: "0x22228e62EfEaC31da195257b4D432A0CcC707942",
120
+ memoryFee2: "0x76eAdF20b3Bf3e7e172A875C866bc4C796BC8828",
121
+ velocityEvolve: "0x0BdE0e84a8b1Faee372616AE069c1b85D74386CC",
122
+ velocityEvolveFee1: "0xAea32E49E128eE3472fe4CD962515Ec96f758b1E",
123
+ velocityEvolveFee2: "0xF73b51849495788773A1433C9980EAC1983F8cF3",
124
+ },
107
125
  };
108
126
  /** Tickers payWith / receive accept, besides "eth" and "usdg" (the catalog's list is only a fallback). */
109
127
  const KNOWN_ASSETS = {
@@ -937,7 +955,7 @@ async function convertAtUsd(c, from, to, amount) {
937
955
  return Number.isFinite(v) && v > 0 ? parseUnits(v.toFixed(to.decimals), to.decimals) : null;
938
956
  }
939
957
 
940
- /** Whether hookit.fun's Relay proxy swaps `from` into `to` on Ink: one side must be ETH or USDG. */
958
+ /** Whether a Relay leg may swap `from` into `to` on Ink: one side must be ETH or USDG, as on hookit.fun. */
941
959
  function relayable(c, from, to) {
942
960
  if (same(from, to)) return false;
943
961
  const funding = (a) => same(a, zeroAddress) || same(a, at(c, "usdg"));
@@ -1178,6 +1196,59 @@ function feeOf(receipt) {
1178
1196
 
1179
1197
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1180
1198
 
1199
+ /** Relay's API, asked directly: hookit.fun never shapes the calls this wallet signs. */
1200
+ const RELAY_API = (process.env.HOOKIT_RELAY_API_URL || "https://api.relay.link").replace(/\/$/, "");
1201
+ const RELAY_API_KEY = process.env.HOOKIT_RELAY_API_KEY?.trim();
1202
+
1203
+ /**
1204
+ * An Ink EXACT_INPUT quote for this wallet from Relay's API: { body } with the quote's requestId, amountOut,
1205
+ * minimumAmount, impactPercent and calls (every transaction step's items), or { error, status }.
1206
+ */
1207
+ async function relayFetchQuote(from, to, amount, slippageBps) {
1208
+ let res;
1209
+ try {
1210
+ res = await fetch(`${RELAY_API}/quote/v2`, {
1211
+ method: "POST",
1212
+ headers: { "content-type": "application/json", ...(RELAY_API_KEY ? { "x-api-key": RELAY_API_KEY } : {}) },
1213
+ body: JSON.stringify({
1214
+ user: account.address,
1215
+ recipient: account.address,
1216
+ originChainId: ink.id,
1217
+ destinationChainId: ink.id,
1218
+ originCurrency: from,
1219
+ destinationCurrency: to,
1220
+ amount: amount.toString(),
1221
+ tradeType: "EXACT_INPUT",
1222
+ slippageTolerance: String(slippageBps),
1223
+ }),
1224
+ });
1225
+ } catch {
1226
+ return { error: "Relay's quote API did not answer. Retry later.", status: 0 };
1227
+ }
1228
+ const payload = await res.json().catch(() => null);
1229
+ if (!res.ok || !payload || payload.errorCode) {
1230
+ return { error: typeof payload?.message === "string" ? payload.message.slice(0, 200) : `Relay answered ${res.status}`, status: res.status };
1231
+ }
1232
+ const steps = Array.isArray(payload.steps) ? payload.steps : [];
1233
+ // A signature step (permit, authorize) cannot be sent as a transaction.
1234
+ if (steps.some((step) => step?.kind != null && step.kind !== "transaction")) return { error: "the route needs a signature", status: 502 };
1235
+ const calls = steps
1236
+ .flatMap((step) => (Array.isArray(step?.items) ? step.items : []))
1237
+ .map((item) => item?.data)
1238
+ .filter((data) => data && typeof data === "object")
1239
+ .map((data) => ({ chainId: Number(data.chainId), to: data.to, data: data.data, value: String(data.value ?? "0") }));
1240
+ const out = payload.details?.currencyOut;
1241
+ return {
1242
+ body: {
1243
+ requestId: payload.requestId ?? "",
1244
+ amountOut: out?.amount,
1245
+ minimumAmount: /^\d+$/.test(out?.minimumAmount ?? "") ? out.minimumAmount : out?.amount,
1246
+ impactPercent: payload.details?.totalImpact?.percent ?? null,
1247
+ calls,
1248
+ },
1249
+ };
1250
+ }
1251
+
1181
1252
  /**
1182
1253
  * Relay's contracts on Ink (api.relay.link/chains): a conversion only ever calls one of these, or approves one of them
1183
1254
  * on the token it spends. A quote that asks for anything else is refused before signing.
@@ -1231,37 +1302,19 @@ async function relayFairOut(inMeta, outMeta, amount) {
1231
1302
  }
1232
1303
 
1233
1304
  /**
1234
- * A Relay quote to swap `amount` of `from` into `to` on Ink for this wallet, through hookit.fun's /api/relay/quote
1235
- * (Relay's key stays on that server): { quote: { requestId, out, min, impact, calls } } or { error }.
1305
+ * A Relay quote to swap `amount` of `from` into `to` on Ink for this wallet, from Relay's API:
1306
+ * { quote: { requestId, out, min, impact, calls } } or { error }.
1236
1307
  */
1237
1308
  async function relayQuote(from, to, amount, slippageBps) {
1238
1309
  const [inMeta, outMeta] = await Promise.all([assetMeta(from), assetMeta(to)]);
1239
- let res;
1240
- try {
1241
- res = await fetch(`${API}/api/relay/quote`, {
1242
- method: "POST",
1243
- headers: { "content-type": "application/json" },
1244
- body: JSON.stringify({
1245
- user: account.address,
1246
- originChainId: ink.id,
1247
- destinationChainId: ink.id,
1248
- originCurrency: from,
1249
- destinationCurrency: to,
1250
- amount: amount.toString(),
1251
- slippageBps,
1252
- }),
1253
- });
1254
- } catch {
1255
- return { error: "hookit.fun's Relay quote did not answer. Retry later." };
1256
- }
1257
- const body = await res.json().catch(() => null);
1258
- if (!res.ok) {
1259
- const why = body?.error ?? `hookit.fun answered ${res.status}`;
1260
- if (why === "Unsupported Relay pair") return { error: `Relay does not swap ${inMeta.symbol} into ${outMeta.symbol} through hookit.fun.`, unsupported: true };
1261
- return { error: `Relay has no ${inMeta.symbol} to ${outMeta.symbol} route right now (${why}).` };
1310
+ const fetched = await relayFetchQuote(from, to, amount, slippageBps);
1311
+ if (fetched.error) {
1312
+ if (fetched.status === 0) return { error: fetched.error };
1313
+ return { error: `Relay has no ${inMeta.symbol} to ${outMeta.symbol} route right now (${fetched.error}).` };
1262
1314
  }
1315
+ const { body } = fetched;
1263
1316
  if (!/^\d+$/.test(body?.amountOut ?? "") || !/^\d+$/.test(body?.minimumAmount ?? "") || !Array.isArray(body?.calls)) {
1264
- return { error: "hookit.fun's Relay quote came back malformed. Nothing was sent." };
1317
+ return { error: "Relay's quote came back malformed. Nothing was sent." };
1265
1318
  }
1266
1319
  const out = BigInt(body.amountOut);
1267
1320
  if (out <= 0n) return { error: `Relay would return no ${outMeta.symbol}.` };
@@ -1343,24 +1396,28 @@ const ETH_TRACE = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
1343
1396
  /**
1344
1397
  * Runs the approval and Relay's transactions together in one eth_simulateV1 from this wallet (nothing is sent) and
1345
1398
  * checks what the wallet would send and get: at most `amount` of `from` out, at least `min` of `to` in, nothing else
1346
- * out. Relay contracts run arbitrary calls, so this is what stops a forged quote from paying someone else. null when
1347
- * sound, else why not.
1399
+ * out. Relay contracts run arbitrary calls, so this is what stops a forged quote from paying someone else. The
1400
+ * simulation is validated and priced with `fees`, the fee fields the swap is then signed with, so a called contract
1401
+ * cannot tell it from the real transaction by a zero gas price or base fee. null when sound, else why not.
1348
1402
  */
1349
- async function relaySimulate(from, to, amount, min, plan) {
1403
+ async function relaySimulate(from, to, amount, min, plan, fees) {
1350
1404
  const wallet = account.address;
1405
+ const priced = { maxFeePerGas: toHex(fees.maxFeePerGas), maxPriorityFeePerGas: toHex(fees.maxPriorityFeePerGas) };
1351
1406
  const calls = [];
1352
1407
  if (from !== zeroAddress && plan.spender) {
1353
- calls.push({ from: wallet, to: from, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [plan.spender, amount] }) });
1408
+ calls.push({ from: wallet, to: from, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [plan.spender, amount] }), ...priced });
1354
1409
  }
1355
- for (const tx of plan.txs) calls.push({ from: wallet, to: tx.to, data: tx.data, value: toHex(tx.value) });
1410
+ for (const tx of plan.txs) calls.push({ from: wallet, to: tx.to, data: tx.data, value: toHex(tx.value), ...priced });
1356
1411
  let result;
1357
1412
  try {
1358
1413
  result = await publicClient.request({
1359
1414
  method: "eth_simulateV1",
1360
- params: [{ blockStateCalls: [{ calls }], traceTransfers: true, validation: false }, "latest"],
1415
+ params: [{ blockStateCalls: [{ calls }], traceTransfers: true, validation: true }, "latest"],
1361
1416
  });
1362
1417
  } catch (err) {
1363
- return `this RPC cannot simulate Relay's transactions (eth_simulateV1: ${shortError(err)}). Set HOOKIT_RPC_URL to an Ink RPC that supports it, such as https://rpc-gel.inkonchain.com.`;
1418
+ const why = shortError(err);
1419
+ if (/insufficient funds|nonce|fee cap|gas limit/i.test(why)) return `Relay's swap fails when simulated (${why})`;
1420
+ return `this RPC cannot simulate Relay's transactions (eth_simulateV1: ${why}). Set HOOKIT_RPC_URL to an Ink RPC that supports it, such as https://rpc-gel.inkonchain.com.`;
1364
1421
  }
1365
1422
  const net = new Map();
1366
1423
  const add = (asset, v) => net.set(asset, (net.get(asset) ?? 0n) + v);
@@ -1384,12 +1441,18 @@ async function relaySimulate(from, to, amount, min, plan) {
1384
1441
  return null;
1385
1442
  }
1386
1443
 
1387
- /** The structural and the simulated check of a Relay quote: { plan } or { error } (refused, nothing sent). */
1444
+ /**
1445
+ * The structural and the simulated check of a Relay quote: { plan, fees } or { error } (refused, nothing sent). `fees`
1446
+ * are the fee fields the check simulated with; the swap must be signed with them.
1447
+ */
1388
1448
  async function relayCheck(from, to, amount, quote) {
1389
1449
  const plan = relayPlan(quote.calls, from, amount, quote.inMeta.symbol);
1390
1450
  if (plan.error) return plan;
1391
- const why = await relaySimulate(from, to, amount, quote.min, plan);
1392
- return why ? { error: `Refused Relay's transactions: ${why}. Nothing was sent.` } : { plan };
1451
+ const estimated = await publicClient.estimateFeesPerGas().catch(() => null);
1452
+ if (!estimated?.maxFeePerGas) return { error: "Could not read Ink's gas price, so Relay's swap cannot be simulated. Nothing was sent." };
1453
+ const fees = { maxFeePerGas: estimated.maxFeePerGas, maxPriorityFeePerGas: estimated.maxPriorityFeePerGas ?? 0n };
1454
+ const why = await relaySimulate(from, to, amount, quote.min, plan, fees);
1455
+ return why ? { error: `Refused Relay's transactions: ${why}. Nothing was sent.` } : { plan, fees };
1393
1456
  }
1394
1457
 
1395
1458
  /** Sets this wallet's allowance of `asset` to `spender` to exactly `amount` (0 revokes). { hash } or { error }. */
@@ -1436,7 +1499,7 @@ async function relayStatus(requestId) {
1436
1499
  async function relayConvert(from, to, amount, quote) {
1437
1500
  const checked = await relayCheck(from, to, amount, quote);
1438
1501
  if (checked.error) return { error: checked.error, txs: [] };
1439
- const { plan } = checked;
1502
+ const { plan, fees: feeFields } = checked;
1440
1503
  const txs = [];
1441
1504
  const receipts = [];
1442
1505
  const approves = from !== zeroAddress && plan.spender;
@@ -1452,13 +1515,14 @@ async function relayConvert(from, to, amount, quote) {
1452
1515
  for (const tx of plan.txs) {
1453
1516
  let gas;
1454
1517
  try {
1455
- await publicClient.call({ account, ...tx });
1456
- const estimated = await publicClient.estimateGas({ account, ...tx });
1518
+ await publicClient.call({ account, ...tx, ...feeFields });
1519
+ const estimated = await publicClient.estimateGas({ account, ...tx, ...feeFields });
1457
1520
  gas = estimated + estimated / 3n;
1458
1521
  } catch (err) {
1459
1522
  return { error: `Relay's swap would fail: ${shortError(err)}`, txs };
1460
1523
  }
1461
- const hash = await walletClient.sendTransaction({ ...tx, gas: gas > MAX_TX_GAS ? MAX_TX_GAS : gas });
1524
+ // Signed with the fee fields the swap was simulated with.
1525
+ const hash = await walletClient.sendTransaction({ ...tx, ...feeFields, gas: gas > MAX_TX_GAS ? MAX_TX_GAS : gas });
1462
1526
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
1463
1527
  txs.push(hash);
1464
1528
  receipts.push(receipt);
@@ -1749,6 +1813,87 @@ function launchRefusal(prepared) {
1749
1813
  return null;
1750
1814
  }
1751
1815
 
1816
+ /** CreatorPayoutRouterC's provider ids (web/src/lib/social-payout.ts). */
1817
+ const PAYOUT_PROVIDER = { wallet: 0, github: 1, x: 2, tiktok: 3 };
1818
+
1819
+ /** "@Name", "name" or a profile URL → "name", as hookit.fun reads a payout handle; null when not one. */
1820
+ function payoutHandle(kind, raw) {
1821
+ if (typeof raw !== "string" || raw.length > 200) return null;
1822
+ const handle = raw
1823
+ .trim()
1824
+ .replace(/^https?:\/\/(www\.)?(github\.com|x\.com|twitter\.com|tiktok\.com)\//i, "")
1825
+ .replace(/^@/, "")
1826
+ .replace(/[/?#].*$/, "");
1827
+ const pattern = { github: /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/, x: /^[A-Za-z0-9_]{1,15}$/, tiktok: /^[A-Za-z0-9_.]{2,24}$/ }[kind];
1828
+ return pattern?.test(handle) ? handle : null;
1829
+ }
1830
+
1831
+ /** A GitHub account's numeric id from api.github.com (not hookit.fun), so a forged lookup cannot pick the payee. */
1832
+ async function githubUserId(handle) {
1833
+ try {
1834
+ const res = await fetch(`https://api.github.com/users/${encodeURIComponent(handle)}`, {
1835
+ headers: { accept: "application/vnd.github+json", "user-agent": "hookit-mcp" },
1836
+ });
1837
+ const body = await res.json().catch(() => null);
1838
+ return res.ok && Number.isSafeInteger(body?.id) && body.id > 0 ? String(body.id) : null;
1839
+ } catch {
1840
+ return null;
1841
+ }
1842
+ }
1843
+
1844
+ /**
1845
+ * Who the launch hookit.fun prepared pays the creator fees to, read from its calldata: { kind: "self" } through Factory
1846
+ * C, else the payout router's { provider, accountId, wallet }; null when the router call does not decode.
1847
+ */
1848
+ function launchPayoutOf(prepared) {
1849
+ if (same(prepared.tx?.to, PINNED.factory)) return { kind: "self" };
1850
+ try {
1851
+ const [, provider, accountId, wallet] = decodeFunctionData({ abi: payoutRouterLaunchAbi, data: prepared.tx.data }).args;
1852
+ return { kind: "router", provider, accountId, wallet };
1853
+ } catch {
1854
+ return null;
1855
+ }
1856
+ }
1857
+
1858
+ /**
1859
+ * Why the launch must not be signed because it pays someone other than `payout` asked, or null. A TikTok handle is
1860
+ * hashed here and a GitHub one looked up before preparing, so only an X handle without accountId takes hookit.fun's
1861
+ * lookup (the result shows the id the calldata pays).
1862
+ */
1863
+ function launchPayoutRefusal(prepared, payout) {
1864
+ const got = launchPayoutOf(prepared);
1865
+ const kind = payout?.kind ?? "wallet";
1866
+ const refuse = (who) => `hookit answered with a launch that pays the creator fees to someone else than ${who}. Nothing was sent.`;
1867
+ if (!got) return refuse("asked (its payout router call does not decode)");
1868
+ if (kind === "wallet") {
1869
+ if (!payout?.address || same(payout.address, account?.address)) return got.kind === "self" ? null : refuse("this wallet");
1870
+ const paid = got.kind === "router" && got.provider === PAYOUT_PROVIDER.wallet && got.accountId === 0n;
1871
+ return paid && same(got.wallet, payout.address) ? null : refuse(payout.address);
1872
+ }
1873
+ const who = payout.handle ? `@${payout.handle} on ${kind}` : `${kind} account ${payout.accountId}`;
1874
+ if (got.kind !== "router" || got.provider !== PAYOUT_PROVIDER[kind] || !same(got.wallet, zeroAddress)) return refuse(who);
1875
+ let expected = null;
1876
+ try {
1877
+ if (payout.accountId) expected = BigInt(payout.accountId);
1878
+ else if (kind === "tiktok") {
1879
+ const handle = payoutHandle("tiktok", payout.handle);
1880
+ expected = handle ? BigInt(keccak256(toHex(`tiktok:${handle.toLowerCase()}`))) : null;
1881
+ }
1882
+ } catch {
1883
+ return refuse(who);
1884
+ }
1885
+ if (expected !== null) return got.accountId === expected ? null : refuse(who);
1886
+ return kind === "x" && got.accountId > 0n ? null : refuse(who);
1887
+ }
1888
+
1889
+ /** The creator-fee payee as the signed calldata has it (never hookit.fun's plan). */
1890
+ function launchPayoutView(prepared) {
1891
+ const got = launchPayoutOf(prepared);
1892
+ if (!got || got.kind === "self") return { to: "this wallet" };
1893
+ const kind = Object.keys(PAYOUT_PROVIDER).find((k) => PAYOUT_PROVIDER[k] === got.provider) ?? `provider ${got.provider}`;
1894
+ return kind === "wallet" ? { kind, address: got.wallet } : { kind, accountId: got.accountId.toString() };
1895
+ }
1896
+
1752
1897
  /**
1753
1898
  * Runs the tools that sign one at a time: an agent may call several at once, and two of them sharing the wallet's
1754
1899
  * nonce or its balance deltas could spend what the other one just received.
@@ -2157,9 +2302,20 @@ server.registerTool(
2157
2302
  } else {
2158
2303
  delete args.builderHooks;
2159
2304
  }
2305
+ // Who is paid is checked in the calldata hookit.fun prepares; a GitHub handle is pinned to its id here first.
2306
+ if (args.payout?.kind === "github" && !args.payout.accountId) {
2307
+ const handle = payoutHandle("github", args.payout.handle);
2308
+ const id = handle ? await githubUserId(handle) : null;
2309
+ if (!id) return fail("Could not look up that GitHub handle on api.github.com. Pass payout.accountId (the account's numeric id) instead.");
2310
+ args.payout = { ...args.payout, accountId: id };
2311
+ }
2312
+ // The current Evolve's module id (v2 once it has code, else v1), read from the ModuleRegistry by plugin address.
2313
+ const evolveModuleId = args.evolve ? await currentEvolveModuleId() : null;
2314
+ if (args.evolve && evolveModuleId === null) return fail("Evolve is not registered in the ModuleRegistry (or is deprecated or halted): launch without evolve.");
2160
2315
  const refusalOf = (prepared) =>
2161
2316
  launchRefusal(prepared) ??
2162
- (args.evolve ? evolveLaunchRefusal(prepared, args) : null) ??
2317
+ launchPayoutRefusal(prepared, args.payout) ??
2318
+ (args.evolve ? evolveLaunchRefusal(prepared, args, evolveModuleId) : null) ??
2163
2319
  (velocity ? velocityLaunchRefusal(prepared, velocity.asked, velocity.ids) : null) ??
2164
2320
  (builder ? builderHooksLaunchRefusal(prepared, builder.expected, builder.entries) : null) ??
2165
2321
  (!args.evolve && !velocity && !builder ? unaskedSlotsRefusal(prepared) : null);
@@ -2188,6 +2344,7 @@ server.registerTool(
2188
2344
  ? `The launch would fail: ${launchSimulationError(prepared.simulation.error)}`
2189
2345
  : "The launch is valid.",
2190
2346
  plan: prepared.plan,
2347
+ payout: launchPayoutView(prepared),
2191
2348
  ...(args.evolve ? { evolve: evolveLaunchView(prepared) } : {}),
2192
2349
  ...(velocity ? { velocity: velocityLaunchView(prepared) } : {}),
2193
2350
  ...(builder ? { builderHooks: builderHooksLaunchView(prepared, builder.expected, builder.entries) } : {}),
@@ -2225,7 +2382,7 @@ server.registerTool(
2225
2382
  page: token ? `${API}/token/${token}` : null,
2226
2383
  tx: `https://explorer.inkonchain.com/tx/${hash}`,
2227
2384
  settings: prepared.plan.settings,
2228
- payout: prepared.plan.payout,
2385
+ payout: launchPayoutView(prepared),
2229
2386
  payoutAgent: agent,
2230
2387
  note: agent
2231
2388
  ? "Creator fees go to the payout target through this launch's CreatorPayoutAgent. This wallet is its controller."
@@ -2580,7 +2737,7 @@ server.registerTool(
2580
2737
  let firstError = null;
2581
2738
  // Land where the coin is cheapest: the markets of one coin can be priced far apart.
2582
2739
  const landings = (await landingAssets(c, coin, asset)).filter((a) => relayable(c, asset, a.address));
2583
- if (!landings.length) firstError = `Relay does not swap ${pay.symbol} into what this coin trades in through hookit.fun (one side must be ETH or USDG).`;
2740
+ if (!landings.length) firstError = `Relay does not swap ${pay.symbol} into what this coin trades in (one side must be ETH or USDG).`;
2584
2741
  for (const landing of (await rankBuyLandings(c, coin, pay, value, landings)).slice(0, 3)) {
2585
2742
  const q = await relayQuote(asset, landing.address, value, relayBps);
2586
2743
  const checked = q.quote ? await relayCheck(asset, landing.address, value, q.quote) : q;
@@ -2797,7 +2954,7 @@ server.registerTool(
2797
2954
  );
2798
2955
  }
2799
2956
  const assets = await tradeAssets(c, coin);
2800
- const none = landings.length ? "Relay cannot swap what this coin trades in." : `Relay does not swap what this coin trades in into ${out.symbol} through hookit.fun (one side must be ETH or USDG).`;
2957
+ const none = landings.length ? "Relay cannot swap what this coin trades in." : `Relay does not swap what this coin trades in into ${out.symbol} (one side must be ETH or USDG).`;
2801
2958
  return fail(`${relayError ?? firstError ?? none} This coin trades in ${assets.join(", ")}: receive ${assets.length > 1 ? "one of those" : "that"} instead.`);
2802
2959
  }
2803
2960
  const capped = await relayCapRefusal(c, leg, route.out);
@@ -3388,7 +3545,10 @@ const EVOLVE_PARAMS = parseAbiParameters(
3388
3545
  );
3389
3546
 
3390
3547
  /** Evolve's id in ModuleRegistry (v2 module 5, as its tx list records it). */
3548
+ /** Evolve v1's ModuleRegistry id (deprecated by the v2 deployment); a launch is checked against the current Evolve's id. */
3391
3549
  const EVOLVE_MODULE_ID = 5;
3550
+ /** `abi.encode(EvolvePlugin.Params)`: 9 static words. hookit-mcp never plugs hooks at launch (Evolve v2's packed plugs). */
3551
+ const EVOLVE_PARAMS_BYTES = 288;
3392
3552
  /** hookit.fun's defaults for what launch_token's evolve leaves out (web/src/lib/evolve-params.ts): cap 3 %, fee 1 %. */
3393
3553
  const EVOLVE_DEFAULT_MAX_FEE_BPS = 300;
3394
3554
  const EVOLVE_DEFAULT_FEE_BPS = 100;
@@ -3420,7 +3580,8 @@ function evolveLaunchDecoded(prepared) {
3420
3580
  if (cfg.plugins.length !== 1) return { slots: cfg.plugins.length };
3421
3581
  const slot = cfg.plugins[0];
3422
3582
  const [params] = decodeAbiParameters(EVOLVE_PARAMS, slot.initData);
3423
- return { slots: 1, moduleId: Number(slot.moduleId), potBps: Number(slot.potBps), params };
3583
+ const initBytes = (String(slot.initData).length - 2) / 2;
3584
+ return { slots: 1, moduleId: Number(slot.moduleId), potBps: Number(slot.potBps), params, initBytes };
3424
3585
  } catch {
3425
3586
  return null;
3426
3587
  }
@@ -3434,14 +3595,19 @@ function evolveLaunchDecoded(prepared) {
3434
3595
  * the pot when no preset but clean and no `modules` key routes the pot elsewhere, else Factory C's rule that every
3435
3596
  * route totals 100 %).
3436
3597
  */
3437
- function evolveLaunchRefusal(prepared, args = {}) {
3598
+ function evolveLaunchRefusal(prepared, args = {}, evolveModuleId = EVOLVE_MODULE_ID) {
3438
3599
  const nothing = " Nothing was sent; update hookit-mcp or launch without evolve.";
3439
3600
  const d = evolveLaunchDecoded(prepared);
3440
3601
  if (!d || d.slots !== 1 || !same(d.params.decider, account.address)) {
3441
3602
  return `hookit answered with an Evolve launch whose decider is not this wallet (or with another plugin slot than Evolve's).${nothing}`;
3442
3603
  }
3443
- if (d.moduleId !== EVOLVE_MODULE_ID) {
3444
- return `hookit answered with a launch whose plugin slot is module ${d.moduleId}, not Evolve (module ${EVOLVE_MODULE_ID}).${nothing}`;
3604
+ if (d.moduleId !== evolveModuleId) {
3605
+ return `hookit answered with a launch whose plugin slot is module ${d.moduleId}, not Evolve (module ${evolveModuleId}).${nothing}`;
3606
+ }
3607
+ // Audit L-1: bytes after the Params are hooks plugged at launch (Evolve v2), which this server never asks for, and
3608
+ // which Evolve v1 would ignore silently.
3609
+ if (d.initBytes !== EVOLVE_PARAMS_BYTES) {
3610
+ return `hookit answered with ${d.initBytes} bytes of Evolve launch data, not the ${EVOLVE_PARAMS_BYTES} bytes of its settings: hookit-mcp never plugs hooks at launch.${nothing}`;
3445
3611
  }
3446
3612
  const asked = args.evolve && typeof args.evolve === "object" ? args.evolve : {};
3447
3613
  const bps = (v) => (v === undefined || v === null ? undefined : Math.round(Number(v) * 100));
@@ -3526,9 +3692,108 @@ function evolveReason(raw) {
3526
3692
  return t;
3527
3693
  }
3528
3694
 
3695
+ /** An Evolve pin as an address, or null when it is unset, a placeholder, zero or not an address. */
3696
+ function pinnedAddress(a) {
3697
+ return typeof a === "string" && isAddress(a) && BigInt(a) !== 0n ? getAddress(a) : null;
3698
+ }
3699
+
3700
+ /** HOOKIT_EVOLVE_PLUGIN (v2) and HOOKIT_EVOLVE_V2_*: override the Evolve v2 pins; not an address: off. */
3701
+ {
3702
+ const raw = process.env.HOOKIT_EVOLVE_PLUGIN?.trim();
3703
+ if (raw) PINNED.evolvePlugin = isAddress(raw) ? getAddress(raw) : null;
3704
+ for (const [name, env] of [
3705
+ ["memoryEvolve", "HOOKIT_EVOLVE_V2_MEMORY_EVOLVE"],
3706
+ ["memoryLens", "HOOKIT_EVOLVE_V2_MEMORY_LENS"],
3707
+ ["memoryFee1", "HOOKIT_EVOLVE_V2_MEMORY_FEE1"],
3708
+ ["memoryFee2", "HOOKIT_EVOLVE_V2_MEMORY_FEE2"],
3709
+ ["velocityEvolve", "HOOKIT_EVOLVE_V2_VELOCITY_EVOLVE"],
3710
+ ["velocityEvolveFee1", "HOOKIT_EVOLVE_V2_VELOCITY_EVOLVE_FEE1"],
3711
+ ["velocityEvolveFee2", "HOOKIT_EVOLVE_V2_VELOCITY_EVOLVE_FEE2"],
3712
+ ]) {
3713
+ const v = process.env[env]?.trim();
3714
+ if (v) PINNED.evolveV2[name] = isAddress(v) ? getAddress(v) : null;
3715
+ }
3716
+ }
3717
+
3718
+ const evolveCodeCache = new Map();
3719
+ async function evolveHasCode(a) {
3720
+ if (!a) return false;
3721
+ if (evolveCodeCache.get(a)) return true;
3722
+ const code = await publicClient.getCode({ address: a }).catch(() => undefined);
3723
+ const has = !!code && code !== "0x";
3724
+ if (has) evolveCodeCache.set(a, true);
3725
+ return has;
3726
+ }
3727
+
3728
+ /** Every Evolve plugin version, current (v2) first, then v1. */
3729
+ function evolvePlugins() {
3730
+ return [pinnedAddress(PINNED.evolvePlugin), pinnedAddress(PINNED.evolvePluginV1)].filter(Boolean);
3731
+ }
3732
+
3733
+ /** The Evolve new launches go to: v2 once it has code at its address, else v1 (null: none). */
3734
+ async function currentEvolvePlugin() {
3735
+ for (const p of evolvePlugins()) if (await evolveHasCode(p)) return p;
3736
+ return null;
3737
+ }
3738
+
3739
+ /** The current Evolve's ModuleRegistry id, found by scanning the registry for its address (live entries only), or null. */
3740
+ async function currentEvolveModuleId() {
3741
+ const plugin = await currentEvolvePlugin();
3742
+ const registry = pinnedAddress(PINNED.moduleRegistry);
3743
+ if (!plugin || !registry) return null;
3744
+ try {
3745
+ const n = Number(await read(registry, moduleRegistryAbi, "moduleCount"));
3746
+ for (let id = n; id >= 1; id--) {
3747
+ const e = await read(registry, moduleRegistryAbi, "moduleEntry", [id]);
3748
+ if (same(e.plugin, plugin)) return e.deprecated || e.halted ? null : id;
3749
+ }
3750
+ } catch {
3751
+ // fall through
3752
+ }
3753
+ return null;
3754
+ }
3755
+
3756
+ const evolvePluginOfCache = new Map();
3757
+ /**
3758
+ * The Evolve plugin a coin runs on: the first version (v2, then v1) whose `extensionContext(token)` names a decider;
3759
+ * null for a coin no Evolve knows (or not an address). A coin's plugin never changes, so a hit is cached.
3760
+ */
3761
+ async function evolvePluginOf(token) {
3762
+ if (typeof token !== "string" || !isAddress(token)) return null;
3763
+ const key = token.toLowerCase();
3764
+ if (evolvePluginOfCache.has(key)) return evolvePluginOfCache.get(key);
3765
+ for (const p of evolvePlugins()) {
3766
+ if (!(await evolveHasCode(p))) continue;
3767
+ const ctx = await read(p, evolveAbi, "extensionContext", [getAddress(token)]).catch(() => null);
3768
+ if (ctx && !same(ctx[0], zeroAddress)) {
3769
+ evolvePluginOfCache.set(key, p);
3770
+ return p;
3771
+ }
3772
+ }
3773
+ return null;
3774
+ }
3775
+
3776
+ /** The coin's plugin, else the current one (whose reads then answer "This coin does not run Evolve."). */
3777
+ async function evolvePluginFor(token) {
3778
+ return (await evolvePluginOf(token)) ?? (await currentEvolvePlugin()) ?? pinnedAddress(PINNED.evolvePluginV1);
3779
+ }
3780
+
3781
+ /** Whether `a` is the `key` contract (memoryEvolve, velocityEvolveFee1…) of either Evolve version. */
3782
+ function isEvolvePin(a, key) {
3783
+ return [PINNED[key], PINNED.evolveV2[key]].some((p) => {
3784
+ const pin = pinnedAddress(p);
3785
+ return !!pin && same(a, pin);
3786
+ });
3787
+ }
3788
+
3789
+ /** The Memory / Velocity contracts of an Evolve version: v1's pins for v1, the evolveV2 pins for v2. */
3790
+ function evolveExtensionPins(plugin) {
3791
+ return same(plugin, PINNED.evolvePluginV1) ? PINNED : PINNED.evolveV2;
3792
+ }
3793
+
3529
3794
  /** The coin's Evolve state as a swap sees it now (a due setting counted as applied), or { error }. */
3530
3795
  async function evolveCoin(token) {
3531
- const plugin = PINNED.evolvePlugin;
3796
+ const plugin = await evolvePluginFor(token);
3532
3797
  if (!plugin) return { error: "Evolve is not live in this hookit-mcp release yet." };
3533
3798
  let st;
3534
3799
  try {
@@ -3892,7 +4157,7 @@ server.registerTool(
3892
4157
  annotations: { readOnlyHint: true },
3893
4158
  },
3894
4159
  async () => {
3895
- const plugin = PINNED.evolvePlugin;
4160
+ const plugin = await currentEvolvePlugin();
3896
4161
  if (!plugin) return fail("Evolve is not live in this hookit-mcp release yet.");
3897
4162
  const { ext, entries, truncated } = await evolveCatalog(plugin);
3898
4163
  return reply({
@@ -3980,14 +4245,14 @@ function nftStrategyConfigView(cfg) {
3980
4245
  /** A hook's config as the decider sends it, decoded for the hooks this release knows (NFT Strategy, Memory, Boss Raid). */
3981
4246
  async function evolveConfigView(x, cfg, quote) {
3982
4247
  if (same(x.ext, PINNED.evolveNftStrategy)) return { hook: "NFT Strategy", ...nftStrategyConfigView(cfg) };
3983
- if (same(x.ext, PINNED.memoryEvolve)) {
4248
+ if (isEvolvePin(x.ext, "memoryEvolve")) {
3984
4249
  return { hook: "Memory", ...memoryConfigView(cfg, await assetMeta(getAddress(quote)).catch(() => null)) };
3985
4250
  }
3986
- if (same(x.ext, PINNED.memoryFee1) || same(x.ext, PINNED.memoryFee2)) {
4251
+ if (isEvolvePin(x.ext, "memoryFee1") || isEvolvePin(x.ext, "memoryFee2")) {
3987
4252
  return { hook: "Memory fee companion", ...(cfg === "0x" ? { config: "empty, as it must be" } : { raw: cfg, invalid: "a companion takes no config (0x)" }) };
3988
4253
  }
3989
- if (same(x.ext, velocityAddress(PINNED.velocityEvolve))) return { hook: "Velocity", ...velocityConfigView(cfg) };
3990
- if (same(x.ext, velocityAddress(PINNED.velocityEvolveFee1)) || same(x.ext, velocityAddress(PINNED.velocityEvolveFee2))) {
4254
+ if (isEvolvePin(x.ext, "velocityEvolve")) return { hook: "Velocity", ...velocityConfigView(cfg) };
4255
+ if (isEvolvePin(x.ext, "velocityEvolveFee1") || isEvolvePin(x.ext, "velocityEvolveFee2")) {
3991
4256
  return { hook: "Velocity fee companion", ...(cfg === "0x" ? { config: "empty, as it must be" } : { raw: cfg, invalid: "a companion takes no config (0x)" }) };
3992
4257
  }
3993
4258
  if (bytes32Text(x.name) === "Boss Raid") {
@@ -4020,7 +4285,7 @@ server.registerTool(
4020
4285
  if (!account) return needKey();
4021
4286
  if (!isAddress(token)) return fail(`Not an address: ${token}`);
4022
4287
  token = getAddress(token);
4023
- const plugin = PINNED.evolvePlugin;
4288
+ const plugin = await evolvePluginFor(token);
4024
4289
  if (!plugin) return fail("Evolve is not live in this hookit-mcp release yet.");
4025
4290
  const ctx = await read(plugin, evolveAbi, "extensionContext", [token]).catch(() => null);
4026
4291
  if (!ctx) return fail("This coin does not run Evolve.");
@@ -4071,7 +4336,7 @@ server.registerTool(
4071
4336
  return fail(`Hook ${hookId} is already in slot ${j + 1}: one slot per hook.`);
4072
4337
  }
4073
4338
  }
4074
- if (same(x.ext, PINNED.memoryEvolve)) {
4339
+ if (isEvolvePin(x.ext, "memoryEvolve")) {
4075
4340
  // Memory's onAnnounce refuses a decider that is not an AI agent and a setup out of its bounds: say it first.
4076
4341
  if ((await read(x.ext, memoryAbi, "isAgent", [decider]).catch(() => null)) === false) return fail(memoryNotAgent(decider));
4077
4342
  const d = decodeMemorySetup(cfg);
@@ -4079,18 +4344,18 @@ server.registerTool(
4079
4344
  if (refused) return fail(`Memory would refuse this config. ${refused} memory_set builds it from plain settings.`);
4080
4345
  announceAbi = memoryAnnounceAbi;
4081
4346
  hookFailure = memoryFailure;
4082
- } else if (same(x.ext, PINNED.memoryFee1) || same(x.ext, PINNED.memoryFee2)) {
4347
+ } else if (isEvolvePin(x.ext, "memoryFee1") || isEvolvePin(x.ext, "memoryFee2")) {
4083
4348
  if (cfg !== "0x") return fail("A Memory fee companion takes no config (0x).");
4084
4349
  announceAbi = memoryAnnounceAbi;
4085
4350
  hookFailure = memoryFailure;
4086
- } else if (same(x.ext, velocityAddress(PINNED.velocityEvolve))) {
4351
+ } else if (isEvolvePin(x.ext, "velocityEvolve")) {
4087
4352
  // Velocity's onAnnounce refuses a setup out of its bounds and a cashback without a share (or a share without one).
4088
4353
  const d = decodeVelocitySetup(cfg);
4089
4354
  const refused = d.error ?? velocitySetupRefusal(d.setup, shareBps);
4090
4355
  if (refused) return fail(`Velocity would refuse this config. ${refused} velocity_set builds it from a preset and plugs its fee companions.`);
4091
4356
  announceAbi = velocityAnnounceAbi;
4092
4357
  hookFailure = velocityFailure;
4093
- } else if (same(x.ext, velocityAddress(PINNED.velocityEvolveFee1)) || same(x.ext, velocityAddress(PINNED.velocityEvolveFee2))) {
4358
+ } else if (isEvolvePin(x.ext, "velocityEvolveFee1") || isEvolvePin(x.ext, "velocityEvolveFee2")) {
4094
4359
  if (cfg !== "0x") return fail("A Velocity fee companion takes no config (0x).");
4095
4360
  announceAbi = velocityAnnounceAbi;
4096
4361
  hookFailure = velocityFailure;
@@ -4110,12 +4375,13 @@ server.registerTool(
4110
4375
  // Memory leaving a slot (or its announced plug-in dropped): its companions ask while Memory runs the plug-in they
4111
4376
  // follow, which Memory keeps running after it leaves (until another of its plug-ins trades) unless it is retired.
4112
4377
  let memoryNote = null;
4113
- const memoryExt = PINNED.memoryEvolve;
4378
+ const pins = evolveExtensionPins(plugin);
4379
+ const memoryExt = pinnedAddress(pins.memoryEvolve);
4114
4380
  const leavingKey = !memoryExt ? null : cancel ? (announced && same(p.ext, memoryExt) ? p.key : null) : same(l.ext, memoryExt) ? l.key : null;
4115
4381
  const companionSlots =
4116
4382
  leavingKey === null
4117
4383
  ? []
4118
- : [PINNED.memoryFee1, PINNED.memoryFee2].flatMap((f) => {
4384
+ : [pinnedAddress(pins.memoryFee1), pinnedAddress(pins.memoryFee2)].flatMap((f) => {
4119
4385
  const at = f ? memoryCompanionAt(h, f) : null;
4120
4386
  return at && at.index !== i ? [at.index + 1] : [];
4121
4387
  });
@@ -4133,11 +4399,11 @@ server.registerTool(
4133
4399
  }
4134
4400
  }
4135
4401
  // Velocity leaving a slot: its companions keep asking a (fading) malus from its last mean until they are unplugged too.
4136
- const velocityExt = velocityAddress(PINNED.velocityEvolve);
4402
+ const velocityExt = velocityAddress(pins.velocityEvolve);
4137
4403
  const velocityLeaving = !!velocityExt && !cancel && same(l.ext, velocityExt) && !same(h.entries[hookId]?.ext, velocityExt);
4138
4404
  const velocityCompanionSlots = !velocityLeaving
4139
4405
  ? []
4140
- : [PINNED.velocityEvolveFee1, PINNED.velocityEvolveFee2].flatMap((f) => {
4406
+ : [pins.velocityEvolveFee1, pins.velocityEvolveFee2].flatMap((f) => {
4141
4407
  const a = velocityAddress(f);
4142
4408
  const at = a ? memoryCompanionAt(h, a) : null;
4143
4409
  return at?.live && at.index !== i && !at.leaving ? [at.index + 1] : [];
@@ -4232,6 +4498,8 @@ const NFT_EVOLVE_COLLECTIONS_MAX = 64;
4232
4498
  const NFT_EVOLVE_CHALLENGE_SEC = 900;
4233
4499
 
4234
4500
  const NFT_EVOLVE_OFF = "NFT Strategy on Evolve is not live in this hookit-mcp release yet.";
4501
+ const NFT_EVOLVE_V2 =
4502
+ "This coin runs Evolve v2, whose hooks do not list NFT Strategy yet (it comes back as NFT Strategy v3 after its review). evolve_hooks lists what it can plug.";
4235
4503
 
4236
4504
  /** NFT Strategy's catalog entry ({ id, entry }) in EvolveExtensions' hook list, found by its pinned address, or null. */
4237
4505
  function nftEvolveEntry(entries) {
@@ -4325,9 +4593,11 @@ server.registerTool(
4325
4593
  },
4326
4594
  async ({ token }) => {
4327
4595
  const nft = PINNED.evolveNftStrategy;
4328
- const plugin = PINNED.evolvePlugin;
4596
+ // NFT Strategy (f3) is listed on Evolve v1's catalog only; v2 gets NFT Strategy v3 after its audit.
4597
+ const plugin = pinnedAddress(PINNED.evolvePluginV1);
4329
4598
  if (!nft || !plugin) return fail(NFT_EVOLVE_OFF);
4330
4599
  if (token !== undefined && !isAddress(token)) return fail(`Not an address: ${token}`);
4600
+ if (token !== undefined && (await evolvePluginOf(token)) && !same(await evolvePluginOf(token), plugin)) return fail(NFT_EVOLVE_V2);
4331
4601
  const [sweeper, raffle, floatNfts, catalog] = await Promise.all([
4332
4602
  read(nft, nftEvolveAbi, "SWEEPER"),
4333
4603
  read(nft, nftEvolveAbi, "RAFFLE"),
@@ -4434,10 +4704,11 @@ server.registerTool(
4434
4704
  serialized(async ({ token, collection, outcome, sharePct, slot, unplug, cancel, dryRun }) => {
4435
4705
  if (!account) return needKey();
4436
4706
  const nft = PINNED.evolveNftStrategy;
4437
- const plugin = PINNED.evolvePlugin;
4707
+ const plugin = pinnedAddress(PINNED.evolvePluginV1);
4438
4708
  if (!nft || !plugin) return fail(NFT_EVOLVE_OFF);
4439
4709
  if (!isAddress(token)) return fail(`Not an address: ${token}`);
4440
4710
  token = getAddress(token);
4711
+ if ((await evolvePluginOf(token)) && !same(await evolvePluginOf(token), plugin)) return fail(NFT_EVOLVE_V2);
4441
4712
  const ctx = await read(plugin, evolveAbi, "extensionContext", [token]).catch(() => null);
4442
4713
  if (!ctx) return fail("This coin does not run Evolve.");
4443
4714
  const [decider, delay, quote] = ctx;
@@ -5352,17 +5623,21 @@ function memoryGrudge(word, now) {
5352
5623
  return mark === 2 ? { until, rule: Number((b >> 168n) & 0xffn), tag: (b >> 176n) & 0xffffffffn } : null;
5353
5624
  }
5354
5625
 
5355
- let memoryCache = null;
5626
+ /** Memory's contracts per Evolve plugin (v1's pins for v1 coins, the evolveV2 pins for v2 coins). */
5627
+ const memoryCache = new Map();
5356
5628
  /**
5357
5629
  * Memory's contracts once their code is there and they belong together (the lens reads this Memory, Memory runs on
5358
5630
  * Evolve's EvolveExtensions, each companion follows this Memory with its rank): { memory, lens, rules, fees: [{ rank,
5359
5631
  * ext }] }, or null. A companion that is not configured, has no code or is not this Memory's is left out: the fee then
5360
5632
  * stops below its percent.
5361
5633
  */
5362
- async function memoryContracts() {
5363
- if (memoryCache) return memoryCache;
5364
- const { memoryEvolve: memory, memoryLens: lens, evolvePlugin } = PINNED;
5365
- if (!memory || !lens || !evolvePlugin) return null;
5634
+ async function memoryContracts(evolvePlugin = PINNED.evolvePluginV1) {
5635
+ if (!evolvePlugin) return null;
5636
+ if (memoryCache.has(evolvePlugin)) return memoryCache.get(evolvePlugin);
5637
+ const pins = evolveExtensionPins(evolvePlugin);
5638
+ const memory = pinnedAddress(pins.memoryEvolve);
5639
+ const lens = pinnedAddress(pins.memoryLens);
5640
+ if (!memory || !lens) return null;
5366
5641
  const hasCode = async (a) => {
5367
5642
  const code = await publicClient.getCode({ address: a }).catch(() => undefined);
5368
5643
  return !!code && code !== "0x";
@@ -5377,16 +5652,18 @@ async function memoryContracts() {
5377
5652
  ]);
5378
5653
  if (!same(lensOf, memory) || !same(ext, extensions)) return null;
5379
5654
  const fees = [];
5380
- for (const [rank, a] of [
5381
- [1, PINNED.memoryFee1],
5382
- [2, PINNED.memoryFee2],
5655
+ for (const [rank, raw] of [
5656
+ [1, pins.memoryFee1],
5657
+ [2, pins.memoryFee2],
5383
5658
  ]) {
5659
+ const a = pinnedAddress(raw);
5384
5660
  if (!a || !(await hasCode(a))) continue;
5385
5661
  const [m, r] = await Promise.all([read(a, memoryFeeAbi, "MEMORY").catch(() => null), read(a, memoryFeeAbi, "RANK").catch(() => null)]);
5386
5662
  if (same(m, memory) && Number(r) === rank) fees.push({ rank, ext: getAddress(a) });
5387
5663
  }
5388
- memoryCache = { memory: getAddress(memory), lens: getAddress(lens), rules: getAddress(rules), fees };
5389
- return memoryCache;
5664
+ const found = { memory: getAddress(memory), lens: getAddress(lens), rules: getAddress(rules), fees };
5665
+ memoryCache.set(evolvePlugin, found);
5666
+ return found;
5390
5667
  } catch {
5391
5668
  return null;
5392
5669
  }
@@ -5498,11 +5775,11 @@ server.registerTool(
5498
5775
  annotations: { readOnlyHint: true },
5499
5776
  },
5500
5777
  async ({ token }) => {
5501
- const c = await memoryContracts();
5778
+ const c = await memoryContracts(await evolvePluginFor(token));
5502
5779
  if (!c) return fail(MEMORY_OFF);
5503
5780
  if (!isAddress(token)) return fail(`Not an address: ${token}`);
5504
5781
  token = getAddress(token);
5505
- const plugin = PINNED.evolvePlugin;
5782
+ const plugin = await evolvePluginFor(token);
5506
5783
  const ctx = await read(plugin, evolveAbi, "extensionContext", [token]).catch(() => null);
5507
5784
  if (!ctx) return fail("This coin does not run Evolve.");
5508
5785
  const [decider, delay, quoteAddress] = ctx;
@@ -5671,11 +5948,11 @@ server.registerTool(
5671
5948
  },
5672
5949
  serialized(async ({ token, rules, allies, feeCapPct, botTrades, reason, sharePct, slot, cancel, dryRun }) => {
5673
5950
  if (!account) return needKey();
5674
- const c = await memoryContracts();
5951
+ const c = await memoryContracts(await evolvePluginFor(token));
5675
5952
  if (!c) return fail(MEMORY_OFF);
5676
5953
  if (!isAddress(token)) return fail(`Not an address: ${token}`);
5677
5954
  token = getAddress(token);
5678
- const plugin = PINNED.evolvePlugin;
5955
+ const plugin = await evolvePluginFor(token);
5679
5956
  const ctx = await read(plugin, evolveAbi, "extensionContext", [token]).catch(() => null);
5680
5957
  if (!ctx) return fail("This coin does not run Evolve.");
5681
5958
  const [decider, delay, quoteAddress] = ctx;
@@ -5926,14 +6203,14 @@ server.registerTool(
5926
6203
  annotations: { readOnlyHint: true },
5927
6204
  },
5928
6205
  async ({ token, wallet }) => {
5929
- const c = await memoryContracts();
6206
+ const c = await memoryContracts(await evolvePluginFor(token));
5930
6207
  if (!c) return fail(MEMORY_OFF);
5931
6208
  if (!isAddress(token)) return fail(`Not an address: ${token}`);
5932
6209
  if (wallet === undefined && !account) return fail("Give wallet (no key is set, so there is no default wallet).");
5933
6210
  if (wallet !== undefined && !isAddress(wallet)) return fail(`Not an address: ${wallet}`);
5934
6211
  token = getAddress(token);
5935
6212
  wallet = getAddress(wallet ?? account.address);
5936
- const ctx = await read(PINNED.evolvePlugin, evolveAbi, "extensionContext", [token]).catch(() => null);
6213
+ const ctx = await read(await evolvePluginFor(token), evolveAbi, "extensionContext", [token]).catch(() => null);
5937
6214
  if (!ctx) return fail("This coin does not run Evolve.");
5938
6215
  const [coin, wo, quote, coinMeta, block, balance] = await Promise.all([
5939
6216
  read(c.memory, memoryAbi, "coinOf", [token]),
@@ -6032,7 +6309,7 @@ server.registerTool(
6032
6309
  },
6033
6310
  serialized(async ({ token, wallets, dryRun }) => {
6034
6311
  if (!account) return needKey();
6035
- const c = await memoryContracts();
6312
+ const c = await memoryContracts(await evolvePluginFor(token));
6036
6313
  if (!c) return fail(MEMORY_OFF);
6037
6314
  if (!isAddress(token)) return fail(`Not an address: ${token}`);
6038
6315
  token = getAddress(token);
@@ -6040,7 +6317,7 @@ server.registerTool(
6040
6317
  if (bad !== undefined) return fail(`Not an address: ${bad}`);
6041
6318
  const list = [...new Map(wallets.map((x) => [x.toLowerCase(), getAddress(x)])).values()];
6042
6319
  if (list.length > MEMORY_LIMITS.pardons) return fail(`At most ${MEMORY_LIMITS.pardons} wallets per pardon.`);
6043
- const plugin = PINNED.evolvePlugin;
6320
+ const plugin = await evolvePluginFor(token);
6044
6321
  const ctx = await read(plugin, evolveAbi, "extensionContext", [token]).catch(() => null);
6045
6322
  if (!ctx) return fail("This coin does not run Evolve.");
6046
6323
  if (!same(ctx[0], account.address)) return fail(`This wallet does not decide this coin: ${getAddress(ctx[0])} does (the wallet that launched it).`);
@@ -7067,18 +7344,21 @@ async function velocityLaunchCheck(args) {
7067
7344
  return { asked: a.asked, ids };
7068
7345
  }
7069
7346
 
7070
- let velocityEvolveCache = null;
7347
+ /** Velocity on Evolve's contracts per Evolve plugin (v1's pins for v1 coins, the evolveV2 pins for v2 coins). */
7348
+ const velocityEvolveCache = new Map();
7071
7349
  /**
7072
7350
  * Velocity on Evolve once its code is there and it belongs together (Velocity runs on Evolve's EvolveExtensions, each
7073
7351
  * companion follows this Velocity with its rank): { velocity, ext, poolManager, fees: [{ rank, ext }] }, or null. A
7074
7352
  * companion that is not configured, has no code or is not this Velocity's is left out: the malus then stops below its
7075
7353
  * percent.
7076
7354
  */
7077
- async function velocityEvolveContracts() {
7078
- if (velocityEvolveCache) return velocityEvolveCache;
7079
- const velocity = velocityAddress(PINNED.velocityEvolve);
7080
- const evolvePlugin = PINNED.evolvePlugin;
7081
- if (!velocity || !evolvePlugin) return null;
7355
+ async function velocityEvolveContracts(evolvePlugin = PINNED.evolvePluginV1) {
7356
+ if (!evolvePlugin) return null;
7357
+ if (velocityEvolveCache.has(evolvePlugin)) return velocityEvolveCache.get(evolvePlugin);
7358
+ // v1's pins for a v1 coin, the evolveV2 pins for a v2 coin (`evolveExtensionPins`).
7359
+ const pins = same(evolvePlugin, PINNED.evolvePluginV1) ? PINNED : PINNED.evolveV2;
7360
+ const velocity = velocityAddress(pins.velocityEvolve);
7361
+ if (!velocity) return null;
7082
7362
  const hasCode = async (a) => {
7083
7363
  const code = await publicClient.getCode({ address: a }).catch(() => undefined);
7084
7364
  return !!code && code !== "0x";
@@ -7094,8 +7374,8 @@ async function velocityEvolveContracts() {
7094
7374
  if (!same(ext, extensions) || !same(evolve, evolvePlugin)) return null;
7095
7375
  const fees = [];
7096
7376
  for (const [rank, raw] of [
7097
- [1, PINNED.velocityEvolveFee1],
7098
- [2, PINNED.velocityEvolveFee2],
7377
+ [1, pins.velocityEvolveFee1],
7378
+ [2, pins.velocityEvolveFee2],
7099
7379
  ]) {
7100
7380
  const a = velocityAddress(raw);
7101
7381
  if (!a || !(await hasCode(a))) continue;
@@ -7106,8 +7386,9 @@ async function velocityEvolveContracts() {
7106
7386
  ]);
7107
7387
  if (same(v, velocity) && Number(r) === rank && same(x, ext)) fees.push({ rank, ext: a });
7108
7388
  }
7109
- velocityEvolveCache = { velocity, ext: getAddress(ext), poolManager: getAddress(poolManager), fees };
7110
- return velocityEvolveCache;
7389
+ const found = { velocity, ext: getAddress(ext), poolManager: getAddress(poolManager), fees };
7390
+ velocityEvolveCache.set(evolvePlugin, found);
7391
+ return found;
7111
7392
  } catch {
7112
7393
  return null;
7113
7394
  }
@@ -7348,7 +7629,7 @@ function velocityFailure(err) {
7348
7629
 
7349
7630
  /** The coin's Evolve context, its slots and Velocity's catalog ids, for velocity_set and velocity_status: { … } or { error }. */
7350
7631
  async function velocityCoin(c, token) {
7351
- const plugin = PINNED.evolvePlugin;
7632
+ const plugin = await evolvePluginFor(token);
7352
7633
  const ctx = await read(plugin, evolveAbi, "extensionContext", [token]).catch(() => null);
7353
7634
  if (!ctx) return { error: "This coin does not run Evolve. Velocity on Evolve is for Evolve coins; a coin without Evolve gets Velocity Fee at launch (launch_token velocity)." };
7354
7635
  const [decider, delay, quoteAddress] = ctx;
@@ -7389,7 +7670,7 @@ server.registerTool(
7389
7670
  },
7390
7671
  serialized(async ({ token, preset, malusPct, cashback, cashbackPct, sharePct, slot, unplug, cancel, dryRun }) => {
7391
7672
  if (!account) return needKey();
7392
- const c = await velocityEvolveContracts();
7673
+ const c = await velocityEvolveContracts(await evolvePluginFor(token));
7393
7674
  if (!c) return fail(VELOCITY_EVOLVE_OFF);
7394
7675
  if (!isAddress(token)) return fail(`Not an address: ${token}`);
7395
7676
  token = getAddress(token);
@@ -7539,7 +7820,7 @@ server.registerTool(
7539
7820
  annotations: { readOnlyHint: true },
7540
7821
  },
7541
7822
  async ({ token, sellAmount, sellCoins }) => {
7542
- const c = await velocityEvolveContracts();
7823
+ const c = await velocityEvolveContracts(await evolvePluginFor(token));
7543
7824
  if (!c) return fail(VELOCITY_EVOLVE_OFF);
7544
7825
  if (!isAddress(token)) return fail(`Not an address: ${token}`);
7545
7826
  if (sellAmount !== undefined && sellCoins !== undefined) return fail("Give sellAmount or sellCoins, not both.");
@@ -7700,14 +7981,14 @@ server.registerTool(
7700
7981
  annotations: { readOnlyHint: true },
7701
7982
  },
7702
7983
  async ({ token, wallet }) => {
7703
- const c = await velocityEvolveContracts();
7984
+ const c = await velocityEvolveContracts(await evolvePluginFor(token));
7704
7985
  if (!c) return fail(VELOCITY_EVOLVE_OFF);
7705
7986
  if (!isAddress(token)) return fail(`Not an address: ${token}`);
7706
7987
  if (wallet === undefined && !account) return needKey();
7707
7988
  if (wallet !== undefined && !isAddress(wallet)) return fail(`Not an address: ${wallet}`);
7708
7989
  token = getAddress(token);
7709
7990
  const who = getAddress(wallet ?? account.address);
7710
- const ctx = await read(PINNED.evolvePlugin, evolveAbi, "extensionContext", [token]).catch(() => null);
7991
+ const ctx = await read(await evolvePluginFor(token), evolveAbi, "extensionContext", [token]).catch(() => null);
7711
7992
  if (!ctx) return fail("This coin does not run Evolve.");
7712
7993
  const quote = await assetMeta(getAddress(ctx[2]));
7713
7994
  const owed = await read(c.velocity, velocityEvolveAbi, "cashbackOf", [token, who]);
@@ -7736,13 +8017,13 @@ server.registerTool(
7736
8017
  },
7737
8018
  serialized(async ({ token, to, dryRun }) => {
7738
8019
  if (!account) return needKey();
7739
- const c = await velocityEvolveContracts();
8020
+ const c = await velocityEvolveContracts(await evolvePluginFor(token));
7740
8021
  if (!c) return fail(VELOCITY_EVOLVE_OFF);
7741
8022
  if (!isAddress(token)) return fail(`Not an address: ${token}`);
7742
8023
  if (to !== undefined && !isAddress(to)) return fail(`Not an address: ${to}`);
7743
8024
  token = getAddress(token);
7744
8025
  const recipient = getAddress(to ?? account.address);
7745
- const ctx = await read(PINNED.evolvePlugin, evolveAbi, "extensionContext", [token]).catch(() => null);
8026
+ const ctx = await read(await evolvePluginFor(token), evolveAbi, "extensionContext", [token]).catch(() => null);
7746
8027
  if (!ctx) return fail("This coin does not run Evolve.");
7747
8028
  const quote = await assetMeta(getAddress(ctx[2]));
7748
8029
  const owed = await read(c.velocity, velocityEvolveAbi, "cashbackOf", [token, account.address]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hookit-mcp",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Let an AI agent launch a coin on hookit.fun (Ink) with its own hook modules and builders' Workshop hooks, find, buy and sell coins with ETH, USDG or tokenized stocks (AI-agents-only ones too, with a Hookit Agent Pass), redeem or borrow at their Backed Floor, collect its creator fees, steer the fee split, the fee and the hooks of the Evolve coins it launched, and deploy and submit its own hook to the Workshop.",
5
5
  "type": "module",
6
6
  "bin": { "hookit-mcp": "index.js" },