hookit-mcp 0.2.0 → 0.2.2
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 +4 -4
- package/index.js +224 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,13 +30,13 @@ Optional env: `HOOKIT_API_URL` (default `https://www.hookit.fun`), `HOOKIT_RPC_U
|
|
|
30
30
|
| `wallet_status` | The wallet's ETH and USDG, the hookit.fun coins it holds with their value, its open floor loans, and whether it can launch. |
|
|
31
31
|
| `launch_token` | Launches in one call, with an optional `devBuyPct`. `dryRun: true` checks and simulates without signing. |
|
|
32
32
|
| `claim_fees` | Collects the creator fees the wallet earned on coins it launched. |
|
|
33
|
-
| `send_eth` | Sends ETH from the wallet, for example back to a Kraken deposit address on Ink. `dryRun: true` checks only. |
|
|
33
|
+
| `send_eth` | Sends ETH from the wallet, for example back to a Kraken deposit address on Ink. Refuses less than 0.001 ETH, Kraken's minimum deposit (`belowKrakenMinimum: true` for an address that is not Kraken). `dryRun: true` checks only. |
|
|
34
34
|
| `agent_pass_challenge` | Step 1 for AI-agents-only coins: five short tasks for the Hookit Agent Pass check (or "already a holder"). `{ agentId? }` for an ERC-8004 validation (below). |
|
|
35
35
|
| `claim_agent_pass` | Step 2: `{ id, answers, agentId?, requestHash? }` within 30 seconds; signs the answers with the wallet and claims the pass on chain. With `agentId`, gets Hookit's ERC-8004 validation instead (below). |
|
|
36
36
|
| `list_coins` | Coins to trade: newest, top market cap, volume or 24h gainers; filters for AI-agents-only, launched by agents, Backed Floor and Boss Raid coins, a pair, or a name. |
|
|
37
37
|
| `coin_info` | One coin: market data, what it trades in, hooks, Backed Floor price and reserve, loans, launch gate, and the wallet's balance and loans on it. |
|
|
38
|
-
| `buy_token` | Step 3 (or any time): buys a coin, `{ token, amount, payWith?, slippageBps?, dryRun? }`. Pays with ETH, USDG, kBTC
|
|
39
|
-
| `sell_token` | Sells a coin, `{ token, amount, receive?, slippageBps?, dryRun? }`, `amount` in coins, `"max"` or `"50%"`. Receives the coin's pair, USDG through the same on-chain bridges, or anything else through Relay after the sale (two transactions). Says when the Backed Floor pays more. |
|
|
38
|
+
| `buy_token` | Step 3 (or any time): buys a coin, `{ token, amount, payWith?, slippageBps?, dryRun? }`. Pays with ETH, USDG, kBTC or a tokenized stock such as wNVDAx. USDG reaches v2 coins priced in a stock in one transaction (bridged on chain); anything else the coin does not trade in is swapped through Relay first (USDG for a coin priced in ETH, ETH for a coin priced in a stock), then the coin is bought: two transactions. On a coin with several markets the Relay swap lands where the coin is cheapest, and a buy is refused when the market of the asset paid with prices the coin far above its other markets. Checks the coin's launch gate first and sends nothing when the wallet is not allowed yet. Always an exact-input buy, the only kind a capped gate window accepts. |
|
|
39
|
+
| `sell_token` | Sells a coin, `{ token, amount, receive?, slippageBps?, dryRun? }`, `amount` in coins, `"max"` or `"50%"`. Receives the coin's pair, USDG through the same on-chain bridges, or anything else through Relay after the sale (two transactions). A multi-pair coin sells by default in the market that pays the most (most of its markets only pay out what their buyers paid in); when the market of the asset asked for cannot pay, it sells in the best one and swaps through Relay. Says when the Backed Floor pays more. |
|
|
40
40
|
| `redeem_floor` | Burns coins for their Backed Floor value from the floor reserve, `{ token, amount, dryRun? }`: exact payout, no slippage. Compares with selling. |
|
|
41
41
|
| `borrow_against_floor` | Locks coins and borrows their floor value, `{ token, amount, days?, dryRun? }` (7, 14, 30, 60 or 90 days, fee 1% to 7% upfront). v2 coins launched with loans. |
|
|
42
42
|
| `repay_loan` | Repays a floor loan before expiry and gets the coins back, `{ loanId?, dryRun? }`. |
|
|
@@ -47,7 +47,7 @@ Paying or receiving a token approves exactly that amount to the contract that tr
|
|
|
47
47
|
|
|
48
48
|
`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.
|
|
49
49
|
|
|
50
|
-
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 Relay's own minimum, and lose nothing else. 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
|
|
50
|
+
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 Relay's own minimum, and lose nothing else. 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).
|
|
51
51
|
|
|
52
52
|
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.
|
|
53
53
|
|
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// hookit-mcp: lets an AI agent launch a coin on hookit.fun (Ink) with its own hook modules, find coins, buy and sell
|
|
3
|
-
// them with ETH, USDG, kBTC
|
|
3
|
+
// them with ETH, USDG, kBTC or tokenized stocks (swapped through Relay when the coin trades in something else;
|
|
4
4
|
// AI-agents-only coins too, with a Hookit Agent Pass), redeem them at their Backed Floor or borrow against it, and
|
|
5
5
|
// collect its creator fees.
|
|
6
6
|
// The key in HOOKIT_PRIVATE_KEY signs on this machine and is never sent anywhere.
|
|
@@ -34,7 +34,7 @@ import { z } from "zod";
|
|
|
34
34
|
const API = (process.env.HOOKIT_API_URL || "https://www.hookit.fun").replace(/\/$/, "");
|
|
35
35
|
const RPC = process.env.HOOKIT_RPC_URL || "https://rpc-gel.inkonchain.com";
|
|
36
36
|
const KEY = process.env.HOOKIT_PRIVATE_KEY?.trim();
|
|
37
|
-
const VERSION = "0.2.
|
|
37
|
+
const VERSION = "0.2.2";
|
|
38
38
|
const EXPLORER = "https://explorer.inkonchain.com";
|
|
39
39
|
/**
|
|
40
40
|
* The Ink deployments this server signs against. Pinned here, never taken from hookit.fun's catalog: a compromised
|
|
@@ -65,8 +65,11 @@ const KNOWN_ASSETS = {
|
|
|
65
65
|
kbtc: "0x73E0C0d45E048D25Fc26Fa3159b0aA04BfA4Db98",
|
|
66
66
|
khype: "0xAd09Cd20e513E4d8cB78036F77Ab9AfdE8555929",
|
|
67
67
|
waaplx: "0x943BF64D566c32A2Bcd41AC92FB63C111cC9De8f",
|
|
68
|
+
wamznx: "0x910cabdE3EBa7Fc1Ce64fD14bD680b9f60fA0F90",
|
|
68
69
|
wgooglx: "0xf8c5308F80E459bb53d9EbE689854d9cBb2Caa6f",
|
|
70
|
+
wmcdx: "0xc6639026a3a862cd4fcbae3f67cB2D25A2959d37",
|
|
69
71
|
wmstrx: "0x30987adF0B11dc698438a99BA04ec3a1AB2c7EaB",
|
|
72
|
+
wnflxx: "0x7d87fD6A379714194a797c0bBB8B40c30D250856",
|
|
70
73
|
wnvdax: "0xa8ddb5Cd96b5222AFe198316E9A57CAA642850D5",
|
|
71
74
|
wspyx: "0xE7E553Cd128F0011777323A0b44a7b96EA1CB540",
|
|
72
75
|
wtslax: "0xc3FdBe3A68EE5dE461D30415a8165cf9Aefe1171",
|
|
@@ -92,6 +95,8 @@ const SWAP_MIN_GAS = 650_000n;
|
|
|
92
95
|
/** FloorLoanPlugin durations (days) and their upfront fee (bps of the loan). */
|
|
93
96
|
const LOAN_FEES_BPS = { 7: 100, 14: 175, 30: 300, 60: 500, 90: 700 };
|
|
94
97
|
const ETH = { address: zeroAddress, symbol: "ETH", decimals: 18 };
|
|
98
|
+
/** Kraken's minimum ETH deposit on Ink: smaller deposits are marked failed and lost. */
|
|
99
|
+
const KRAKEN_MIN_DEPOSIT_WEI = parseEther("0.001");
|
|
95
100
|
|
|
96
101
|
const ink = defineChain({
|
|
97
102
|
id: 57073,
|
|
@@ -139,7 +144,11 @@ const swapRouterAbi = parseAbi([
|
|
|
139
144
|
/** Uniswap's V4Quoter: what a one-pool exact-input swap returns, hook included (eth_call only). */
|
|
140
145
|
const quoterAbi = parseAbi([
|
|
141
146
|
"function quoteExactInputSingle(((address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) poolKey, bool zeroForOne, uint128 exactAmount, bytes hookData) params) returns (uint256 amountOut, uint256 gasEstimate)",
|
|
147
|
+
"error UnexpectedRevertBytes(bytes revertData)",
|
|
148
|
+
"error NotEnoughLiquidity(bytes32 poolId)",
|
|
142
149
|
]);
|
|
150
|
+
/** NotEnoughLiquidity(bytes32): the pool could not fill the whole amount (V4Quoter wraps it in UnexpectedRevertBytes). */
|
|
151
|
+
const NOT_ENOUGH_LIQUIDITY = "7a5ed734";
|
|
143
152
|
/** BalancedAggregatorC: USDG in or out on a v2 coin, bridged to the market's quote when it is a stock or ETH. */
|
|
144
153
|
const aggregatorAbi = parseAbi([
|
|
145
154
|
"struct RouteLeg { uint8 marketIndex; uint8 venue; uint256 amountIn; uint256 minAmountOut; }",
|
|
@@ -707,13 +716,61 @@ async function launches() {
|
|
|
707
716
|
return body;
|
|
708
717
|
}
|
|
709
718
|
|
|
710
|
-
/**
|
|
719
|
+
/**
|
|
720
|
+
* The USD price of one unit of `asset` (ETH, a stock, kBTC): hookit.fun's price for a coin priced in it, else its
|
|
721
|
+
* USDG pool on Ink (a stock that only backs secondary markets); null if unknown.
|
|
722
|
+
*/
|
|
711
723
|
async function usdPrice(c, asset) {
|
|
712
724
|
if (same(asset, at(c, "usdg"))) return 1;
|
|
713
725
|
const body = await launches().catch(() => null);
|
|
714
726
|
if (asset === zeroAddress) return body?.ethUsd > 0 ? body.ethUsd : null;
|
|
715
727
|
const pool = body?.pools.find((p) => same(p.quoteAddress, asset) && p.quoteUsd > 0);
|
|
716
|
-
|
|
728
|
+
if (pool) return pool.quoteUsd;
|
|
729
|
+
const key = asset.toLowerCase();
|
|
730
|
+
const hit = poolPriceCache.get(key);
|
|
731
|
+
if (hit && Date.now() - hit.at < 30_000) return hit.price;
|
|
732
|
+
const price = await usdgPoolPrice(c, asset).catch(() => null);
|
|
733
|
+
poolPriceCache.set(key, { at: Date.now(), price });
|
|
734
|
+
return price;
|
|
735
|
+
}
|
|
736
|
+
const poolPriceCache = new Map();
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* One unit of `asset` in USDG from its Uniswap v4 USDG pool (the bridge RouteConfigC names for it), or null: a
|
|
740
|
+
* thousandth of a unit sold for USDG, else one USDG spent on it when the USDG side is too thin.
|
|
741
|
+
*/
|
|
742
|
+
async function usdgPoolPrice(c, asset) {
|
|
743
|
+
const bridge = await read(at(c, "routeConfig"), routeConfigAbi, "bridgeOf", [asset]);
|
|
744
|
+
if (!bridge.enabled) return null;
|
|
745
|
+
const usdg = at(c, "usdg");
|
|
746
|
+
const usdgIs0 = BigInt(usdg) < BigInt(asset);
|
|
747
|
+
const poolKey = { currency0: usdgIs0 ? usdg : asset, currency1: usdgIs0 ? asset : usdg, fee: bridge.fee, tickSpacing: bridge.tickSpacing, hooks: bridge.hooks };
|
|
748
|
+
const quote = (zeroForOne, exactAmount) =>
|
|
749
|
+
publicClient
|
|
750
|
+
.simulateContract({ address: at(c, "v4Quoter"), abi: quoterAbi, functionName: "quoteExactInputSingle", args: [{ poolKey, zeroForOne, exactAmount, hookData: "0x" }] })
|
|
751
|
+
.then(({ result }) => result[0])
|
|
752
|
+
.catch(() => 0n);
|
|
753
|
+
const unit = 10n ** BigInt((await assetMeta(asset)).decimals);
|
|
754
|
+
const sold = await quote(!usdgIs0, unit / 1000n);
|
|
755
|
+
if (sold > 0n) return Number(formatUnits(sold * 1000n, 6));
|
|
756
|
+
const bought = await quote(usdgIs0, 1_000_000n);
|
|
757
|
+
return bought > 0n ? Number(unit) / Number(bought) : null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/** `amount` of `from` in units of `to`, through their US dollar prices; null when either has no price. */
|
|
761
|
+
async function convertAtUsd(c, from, to, amount) {
|
|
762
|
+
if (same(from.address, to.address)) return amount;
|
|
763
|
+
const [pf, pt] = await Promise.all([usdPrice(c, from.address), usdPrice(c, to.address)]);
|
|
764
|
+
if (!pf || !pt) return null;
|
|
765
|
+
const v = (Number(formatUnits(amount, from.decimals)) * pf) / pt;
|
|
766
|
+
return Number.isFinite(v) && v > 0 ? parseUnits(v.toFixed(to.decimals), to.decimals) : null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** Whether hookit.fun's Relay proxy swaps `from` into `to` on Ink: one side must be ETH or USDG. */
|
|
770
|
+
function relayable(c, from, to) {
|
|
771
|
+
if (same(from, to)) return false;
|
|
772
|
+
const funding = (a) => same(a, zeroAddress) || same(a, at(c, "usdg"));
|
|
773
|
+
return funding(from) || funding(to);
|
|
717
774
|
}
|
|
718
775
|
|
|
719
776
|
/** The v2 aggregator that trades `coin` against USDG, or null (v1 coins, or a hookit.fun without one). */
|
|
@@ -791,10 +848,97 @@ async function planTrade(c, coin, isBuy, asset, amountIn) {
|
|
|
791
848
|
if (result[0] <= 0n) return { error: `The pool would return nothing for this ${what}.` };
|
|
792
849
|
return { route: { kind: "router", spender: at(c, "swapRouter"), market, zeroForOne, out: result[0] } };
|
|
793
850
|
} catch (err) {
|
|
851
|
+
if (errorName(err) === "NotEnoughLiquidity" || errorText(err).includes(NOT_ENOUGH_LIQUIDITY)) {
|
|
852
|
+
const q = await assetMeta(market.quote);
|
|
853
|
+
return {
|
|
854
|
+
thin: true,
|
|
855
|
+
error: isBuy
|
|
856
|
+
? `The coin's ${q.symbol} market cannot fill a buy this big. Buy less.`
|
|
857
|
+
: `The coin's ${q.symbol} market does not hold enough ${q.symbol} to pay for this sale (a market only pays out what its buyers paid in).`,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
794
860
|
return { error: swapFailure(err, what) };
|
|
795
861
|
}
|
|
796
862
|
}
|
|
797
863
|
|
|
864
|
+
/**
|
|
865
|
+
* The sale of `value` coins through each of `assets` (assets the coin trades in), best first by US dollar value,
|
|
866
|
+
* unpriced ones last: { ranked: [{ asset, route, usd }], firstError, thinOnly }. Markets that cannot pay are left out;
|
|
867
|
+
* `thinOnly` says every one that failed lacked liquidity.
|
|
868
|
+
*/
|
|
869
|
+
async function rankSales(c, coin, value, assets) {
|
|
870
|
+
let firstError = null;
|
|
871
|
+
const scored = await Promise.all(
|
|
872
|
+
assets.map(async (asset, i) => {
|
|
873
|
+
const planned = await planTrade(c, coin, false, asset.address, value);
|
|
874
|
+
if (planned.error) return { i, error: planned.error, thin: planned.thin };
|
|
875
|
+
const price = await usdPrice(c, asset.address);
|
|
876
|
+
return { i, asset, route: planned.route, usd: price ? Number(formatUnits(planned.route.out, asset.decimals)) * price : null };
|
|
877
|
+
}),
|
|
878
|
+
);
|
|
879
|
+
for (const s of scored) if (s.error) firstError ??= s.error;
|
|
880
|
+
const ranked = scored
|
|
881
|
+
.filter((s) => s.route)
|
|
882
|
+
.sort((a, b) => (a.usd !== null && b.usd !== null ? b.usd - a.usd : a.usd !== null ? -1 : b.usd !== null ? 1 : a.i - b.i));
|
|
883
|
+
return { ranked, firstError, thinOnly: scored.every((s) => s.route || s.thin) };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* The most coins one market of `assets` can pay for now, up to `value` (a binary search on quotes): { asset, amount }
|
|
888
|
+
* for the market that takes the most, or null when none takes any.
|
|
889
|
+
*/
|
|
890
|
+
async function largestSale(c, coin, assets, value) {
|
|
891
|
+
const fits = await Promise.all(
|
|
892
|
+
assets.map(async (asset) => {
|
|
893
|
+
let lo = 0n;
|
|
894
|
+
let hi = value;
|
|
895
|
+
for (let i = 0; i < 14 && hi - lo > value / 1000n; i++) {
|
|
896
|
+
const mid = (lo + hi) / 2n;
|
|
897
|
+
if ((await planTrade(c, coin, false, asset.address, mid)).route) lo = mid;
|
|
898
|
+
else hi = mid;
|
|
899
|
+
}
|
|
900
|
+
return { asset, amount: (lo * 98n) / 100n };
|
|
901
|
+
}),
|
|
902
|
+
);
|
|
903
|
+
const best = fits.reduce((a, b) => (b.amount > a.amount ? b : a), { amount: 0n });
|
|
904
|
+
return best.amount > 0n ? best : null;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/** Why no single market takes a sale of `value` coins, and what to sell instead (`most` from largestSale). */
|
|
908
|
+
function saleTooBig(coin, value, most) {
|
|
909
|
+
if (!most) return "None of this coin's markets that could pay for this sale has anything to pay with yet: a market only pays out what its buyers paid in. Receive another asset.";
|
|
910
|
+
return `No single market of this coin can pay for ${amountText(value, coin.meta)} now: a market only pays out what its buyers paid in. The most one takes is about ${amountText(most.amount, coin.meta)} (its ${most.asset.symbol} market): sell that much, then the rest in another call.`;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/** `assets` (where a Relay swap of `amount` of `pay` could land) sorted by how many coins each would buy, best first. */
|
|
914
|
+
async function rankBuyLandings(c, coin, pay, amount, assets) {
|
|
915
|
+
const scored = await Promise.all(
|
|
916
|
+
assets.map(async (asset, i) => {
|
|
917
|
+
const legAmount = await convertAtUsd(c, pay, asset, amount).catch(() => null);
|
|
918
|
+
const planned = legAmount ? await planTrade(c, coin, true, asset.address, legAmount) : null;
|
|
919
|
+
return { i, asset, out: planned?.route ? planned.route.out : -1n };
|
|
920
|
+
}),
|
|
921
|
+
);
|
|
922
|
+
return scored.sort((a, b) => (b.out > a.out ? 1 : b.out < a.out ? -1 : a.i - b.i)).map((s) => s.asset);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Another market of `coin` where the same dollars buy at least a quarter more coins than `out` (what `amount` of
|
|
927
|
+
* `pay` buys in its own market), or null: { asset, out, ratio }. The markets of one coin can drift far apart.
|
|
928
|
+
*/
|
|
929
|
+
async function cheaperMarket(c, coin, pay, amount, out) {
|
|
930
|
+
let best = null;
|
|
931
|
+
for (const m of coin.markets) {
|
|
932
|
+
if (same(m.quote, pay.address)) continue;
|
|
933
|
+
const asset = await assetMeta(m.quote);
|
|
934
|
+
const legAmount = await convertAtUsd(c, pay, asset, amount).catch(() => null);
|
|
935
|
+
if (!legAmount) continue;
|
|
936
|
+
const planned = await planTrade(c, coin, true, asset.address, legAmount);
|
|
937
|
+
if (planned.route && planned.route.out * 4n >= out * 5n && (!best || planned.route.out > best.out)) best = { asset, out: planned.route.out };
|
|
938
|
+
}
|
|
939
|
+
return best ? { ...best, ratio: Number((best.out * 100n) / out) / 100 } : null;
|
|
940
|
+
}
|
|
941
|
+
|
|
798
942
|
/** The call that executes `route` for `amountIn` with at least `minOut` back, to this wallet. */
|
|
799
943
|
async function tradeCall(coin, route, isBuy, asset, amountIn, minOut) {
|
|
800
944
|
if (route.kind === "router") {
|
|
@@ -1384,7 +1528,7 @@ server.registerTool(
|
|
|
1384
1528
|
server.registerTool(
|
|
1385
1529
|
"list_pairs",
|
|
1386
1530
|
{
|
|
1387
|
-
description: "What a coin can be priced in (ETH, USDG,
|
|
1531
|
+
description: "What a coin can be priced in (ETH, USDG, kBTC, tokenized stocks), and which can be combined in a multi-pair launch. Needs no key.",
|
|
1388
1532
|
inputSchema: {},
|
|
1389
1533
|
annotations: { readOnlyHint: true },
|
|
1390
1534
|
},
|
|
@@ -1577,14 +1721,18 @@ server.registerTool(
|
|
|
1577
1721
|
"send_eth",
|
|
1578
1722
|
{
|
|
1579
1723
|
description:
|
|
1580
|
-
"Sends ETH from this wallet on Ink, for example to cash out to the user's Kraken ETH deposit address on the Ink network (get it with the Kraken MCP deposit addresses tool for the \"ETH - Ink\" method only). Kraken's minimum ETH deposit on Ink is 0.001 ETH; smaller deposits fail and the funds are lost, so
|
|
1724
|
+
"Sends ETH from this wallet on Ink, for example to cash out to the user's Kraken ETH deposit address on the Ink network (get it with the Kraken MCP deposit addresses tool for the \"ETH - Ink\" method only). Kraken's minimum ETH deposit on Ink is 0.001 ETH; smaller deposits fail and the funds are lost, so it refuses to send less unless belowKrakenMinimum is set for an address that is not Kraken. Run it with dryRun first and repeat the address and amount to the user before sending.",
|
|
1581
1725
|
inputSchema: {
|
|
1582
1726
|
to: z.string().describe("Destination address on Ink."),
|
|
1583
|
-
amountEth: z.string().describe('Amount in ETH, or "max" to send everything except gas.'),
|
|
1727
|
+
amountEth: z.string().describe('Amount in ETH, or "max" to send everything except gas. At least 0.001 ETH.'),
|
|
1728
|
+
belowKrakenMinimum: z
|
|
1729
|
+
.boolean()
|
|
1730
|
+
.optional()
|
|
1731
|
+
.describe("Allows less than 0.001 ETH. Only for an address that is not a Kraken deposit address: Kraken loses smaller deposits."),
|
|
1584
1732
|
dryRun: z.boolean().optional().describe("Check only. Signs nothing."),
|
|
1585
1733
|
},
|
|
1586
1734
|
},
|
|
1587
|
-
serialized(async ({ to, amountEth, dryRun }) => {
|
|
1735
|
+
serialized(async ({ to, amountEth, belowKrakenMinimum, dryRun }) => {
|
|
1588
1736
|
if (!account) return needKey();
|
|
1589
1737
|
if (!isAddress(to)) return fail(`Not an address: ${to}`);
|
|
1590
1738
|
const [balance, gasPrice] = await Promise.all([publicClient.getBalance({ address: account.address }), publicClient.getGasPrice()]);
|
|
@@ -1601,6 +1749,11 @@ server.registerTool(
|
|
|
1601
1749
|
}
|
|
1602
1750
|
}
|
|
1603
1751
|
if (value <= 0n) return fail(`Nothing to send: the wallet holds ${formatEther(balance)} ETH.`);
|
|
1752
|
+
if (value < KRAKEN_MIN_DEPOSIT_WEI && !belowKrakenMinimum) {
|
|
1753
|
+
return fail(
|
|
1754
|
+
`${formatEther(value)} ETH is under Kraken's 0.001 ETH minimum deposit on Ink: Kraken marks smaller deposits as failed and the ETH is lost. Nothing was sent. Wait until the wallet holds more, or, only if ${to} is not a Kraken deposit address, pass belowKrakenMinimum: true.`,
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1604
1757
|
if (value + gasReserve > balance) {
|
|
1605
1758
|
return fail(`Not enough ETH: the wallet holds ${formatEther(balance)} ETH and needs some left for gas.`);
|
|
1606
1759
|
}
|
|
@@ -1745,7 +1898,10 @@ server.registerTool(
|
|
|
1745
1898
|
}),
|
|
1746
1899
|
);
|
|
1747
1900
|
|
|
1748
|
-
/**
|
|
1901
|
+
/**
|
|
1902
|
+
* The asset a coin trades against by default: its only market's quote, USDG for a multi-pair coin (a sale of a
|
|
1903
|
+
* multi-pair coin without the v2 aggregator goes to the market that pays the most instead: see sell_token).
|
|
1904
|
+
*/
|
|
1749
1905
|
function defaultAsset(c, coin) {
|
|
1750
1906
|
return coin.markets.length === 1 ? coin.markets[0].quote : at(c, "usdg");
|
|
1751
1907
|
}
|
|
@@ -1770,7 +1926,7 @@ server.registerTool(
|
|
|
1770
1926
|
"buy_token",
|
|
1771
1927
|
{
|
|
1772
1928
|
description:
|
|
1773
|
-
"Buys a hookit.fun coin from this wallet, exact-input: you set what you spend. Pay with ETH, USDG, kBTC
|
|
1929
|
+
"Buys a hookit.fun coin from this wallet, exact-input: you set what you spend. Pay with ETH, USDG, kBTC or a tokenized stock such as wNVDAx; by default it pays with the coin's own pair (USDG for a multi-pair coin). USDG buys v2 coins priced in a tokenized stock in one transaction (bridged on chain, best route picked). When the coin does not trade in what you pay with (ETH for a coin priced in a stock, USDG for a coin priced in ETH…), it first swaps it through Relay into an asset the coin trades in, then buys: two transactions, both shown in a dryRun. On a coin with several markets, the Relay swap lands in the market where the coin is cheapest, and a buy is refused when the market of the asset you pay with prices the coin far above its other markets. The wallet signs, so it is what a launch gate checks. Step 3 for AI-agents-only coins: if the coin needs an Agent Pass this wallet does not hold, it sends nothing and says to run agent_pass_challenge then claim_agent_pass first. Paying with a token approves exactly the amount first. Use dryRun to quote without sending.",
|
|
1774
1930
|
inputSchema: {
|
|
1775
1931
|
token: tokenArg,
|
|
1776
1932
|
amount: z
|
|
@@ -1821,7 +1977,10 @@ server.registerTool(
|
|
|
1821
1977
|
let relay = null;
|
|
1822
1978
|
if (!(await directAssets(c, coin)).some((a) => same(a.address, asset))) {
|
|
1823
1979
|
let firstError = null;
|
|
1824
|
-
|
|
1980
|
+
// Land where the coin is cheapest: the markets of one coin can be priced far apart.
|
|
1981
|
+
const landings = (await landingAssets(c, coin, asset)).filter((a) => relayable(c, asset, a.address));
|
|
1982
|
+
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).`;
|
|
1983
|
+
for (const landing of (await rankBuyLandings(c, coin, pay, value, landings)).slice(0, 3)) {
|
|
1825
1984
|
const q = await relayQuote(asset, landing.address, value, relayBps);
|
|
1826
1985
|
const checked = q.quote ? await relayCheck(asset, landing.address, value, q.quote) : q;
|
|
1827
1986
|
if (q.quote && !checked.error) {
|
|
@@ -1845,7 +2004,7 @@ server.registerTool(
|
|
|
1845
2004
|
usd = price === null ? null : Number(formatUnits(legAmount, leg.decimals)) * price;
|
|
1846
2005
|
}
|
|
1847
2006
|
if (usd === null) {
|
|
1848
|
-
return fail(`Could not price ${pay.symbol} in US dollars for the $${MAX_BUY_USD} per-buy cap
|
|
2007
|
+
return fail(`Could not price ${pay.symbol} in US dollars for the $${MAX_BUY_USD} per-buy cap: hookit.fun's coin list has no price for it and no USDG pool on Ink quoted it. Nothing was sent; pay with ETH or USDG instead.`);
|
|
1849
2008
|
}
|
|
1850
2009
|
if (usd > MAX_BUY_USD) {
|
|
1851
2010
|
return fail(`buy_token spends at most $${MAX_BUY_USD} per buy (HOOKIT_MAX_BUY_USD); ${amountText(value, pay)} is about $${usd.toFixed(2)}. Buy less, or in several buys.`);
|
|
@@ -1858,6 +2017,15 @@ server.registerTool(
|
|
|
1858
2017
|
const planned = await planTrade(c, coin, true, leg.address, legAmount);
|
|
1859
2018
|
if (planned.error) return fail(planned.error);
|
|
1860
2019
|
let { route } = planned;
|
|
2020
|
+
if (!relay && route.kind === "router" && coin.markets.length > 1) {
|
|
2021
|
+
const cheaper = await cheaperMarket(c, coin, pay, value, route.out);
|
|
2022
|
+
if (cheaper) {
|
|
2023
|
+
const via = relayable(c, zeroAddress, cheaper.asset.address) ? `, or with ETH or USDG (swapped through Relay into that market)` : "";
|
|
2024
|
+
return fail(
|
|
2025
|
+
`This coin costs about ${cheaper.ratio}x more in its ${pay.symbol} market than in its ${cheaper.asset.symbol} market: ${amountText(value, pay)} buys ${amountText(route.out, coin.meta)} here, and the same dollars buy about ${amountText(cheaper.out, coin.meta)} there. Pay with ${cheaper.asset.symbol}${via}. Nothing was sent.`,
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
1861
2029
|
let minOut = (route.out * (10_000n - bps)) / 10_000n || 1n;
|
|
1862
2030
|
const routeWords = await routeText(c, coin, route, true);
|
|
1863
2031
|
const quote = {
|
|
@@ -1945,11 +2113,11 @@ server.registerTool(
|
|
|
1945
2113
|
"sell_token",
|
|
1946
2114
|
{
|
|
1947
2115
|
description:
|
|
1948
|
-
"Sells a hookit.fun coin from this wallet, exact-input: you set how many coins to sell, \"max\" or a percentage. By default it sells for the coin's own pair (ETH, a stock, kBTC;
|
|
2116
|
+
"Sells a hookit.fun coin from this wallet, exact-input: you set how many coins to sell, \"max\" or a percentage. By default it sells for the coin's own pair (ETH, a stock, kBTC); a multi-pair coin sells in the market that pays the most, for that market's asset (USDG through the aggregator on v2 coins). receive \"usdg\" sells v2 coins priced in a tokenized stock for USDG in one transaction. To receive anything else the coin does not trade in (USDG from a coin priced in ETH, ETH from a coin priced in a stock…), or when the market of the asset asked for cannot pay for the sale, it sells in the market that pays the most, then swaps the proceeds through Relay: two transactions. It approves exactly the coins sold first. If the coin's Backed Floor pays more than the market, it says so: redeem_floor then pays more. Use dryRun to quote without sending.",
|
|
1949
2117
|
inputSchema: {
|
|
1950
2118
|
token: tokenArg,
|
|
1951
2119
|
amount: z.union([z.string(), z.number()]).describe('Coins to sell: "1000000", "max" or "50%". "max" keeps 1 wei of the coin.'),
|
|
1952
|
-
receive: z.string().optional().describe('"eth", "usdg", a pair ticker such as "wNVDAx" or "kBTC", or a token address. Default: the coin\'s own pair.'),
|
|
2120
|
+
receive: z.string().optional().describe('"eth", "usdg", a pair ticker such as "wNVDAx" or "kBTC", or a token address. Default: the coin\'s own pair, or on a multi-pair coin the market that pays the most.'),
|
|
1953
2121
|
slippageBps: slippageArg,
|
|
1954
2122
|
dryRun: dryRunArg,
|
|
1955
2123
|
},
|
|
@@ -1965,7 +2133,7 @@ server.registerTool(
|
|
|
1965
2133
|
if (!asset || same(asset, token)) {
|
|
1966
2134
|
return fail(`Cannot sell for "${receive}". Receive ETH, USDG, a pair ticker from list_pairs or a token address; this coin trades in ${(await tradeAssets(c, coin)).join(", ")}.`);
|
|
1967
2135
|
}
|
|
1968
|
-
|
|
2136
|
+
let out = await assetMeta(asset);
|
|
1969
2137
|
const [balance, ethBalance, reserve] = await Promise.all([
|
|
1970
2138
|
balanceOf(token, account.address),
|
|
1971
2139
|
publicClient.getBalance({ address: account.address }),
|
|
@@ -1983,30 +2151,52 @@ server.registerTool(
|
|
|
1983
2151
|
let leg = out;
|
|
1984
2152
|
let route = null;
|
|
1985
2153
|
let relay = null;
|
|
1986
|
-
|
|
2154
|
+
let thin = null;
|
|
2155
|
+
let picked = null;
|
|
2156
|
+
const direct = await directAssets(c, coin);
|
|
2157
|
+
if (!receive && coin.markets.length > 1 && !(await aggregatorFor(c, coin))) {
|
|
2158
|
+
// A multi-pair coin without the v2 aggregator: sell in the market that pays the most, for its own quote, in one
|
|
2159
|
+
// transaction (USDG when it pays within 2% of the best). Most of its markets only pay what their buyers paid in.
|
|
2160
|
+
const { ranked, firstError, thinOnly } = await rankSales(c, coin, value, direct);
|
|
2161
|
+
if (!ranked.length) return fail(thinOnly ? saleTooBig(coin, value, await largestSale(c, coin, direct, value)) : firstError);
|
|
2162
|
+
const usdg = ranked.find((r) => same(r.asset.address, at(c, "usdg")));
|
|
2163
|
+
const pick = usdg && ranked[0].usd !== null && usdg.usd >= ranked[0].usd * 0.98 ? usdg : ranked[0];
|
|
2164
|
+
out = leg = pick.asset;
|
|
2165
|
+
route = pick.route;
|
|
2166
|
+
if (direct.length > 1) picked = `Sold in the coin's ${pick.asset.symbol} market, the one that pays the most for it now. Pass receive to pick another asset.`;
|
|
2167
|
+
} else if (direct.some((a) => same(a.address, asset))) {
|
|
1987
2168
|
const planned = await planTrade(c, coin, false, asset, value);
|
|
1988
|
-
if (planned.error)
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2169
|
+
if (!planned.error) route = planned.route;
|
|
2170
|
+
else if (planned.thin) thin = planned.error;
|
|
2171
|
+
else return fail(planned.error);
|
|
2172
|
+
}
|
|
2173
|
+
if (!route) {
|
|
2174
|
+
// Sell where it pays the most among the markets Relay can swap from into `asset`, then swap the proceeds.
|
|
2175
|
+
const landings = (await landingAssets(c, coin, asset)).filter((a) => relayable(c, a.address, asset));
|
|
2176
|
+
const { ranked, firstError, thinOnly } = await rankSales(c, coin, value, landings);
|
|
2177
|
+
if (!ranked.length && thinOnly && (thin || landings.length)) {
|
|
2178
|
+
return fail(saleTooBig(coin, value, await largestSale(c, coin, thin ? [out, ...landings] : landings, value)));
|
|
2179
|
+
}
|
|
2180
|
+
let relayError = null;
|
|
2181
|
+
for (const r of ranked.slice(0, 3)) {
|
|
2182
|
+
const q = await relayQuote(r.asset.address, asset, r.route.out, relayBps);
|
|
1999
2183
|
if (q.quote) {
|
|
2000
|
-
leg =
|
|
2001
|
-
route =
|
|
2184
|
+
leg = r.asset;
|
|
2185
|
+
route = r.route;
|
|
2002
2186
|
relay = q.quote;
|
|
2003
2187
|
break;
|
|
2004
2188
|
}
|
|
2005
|
-
if (!q.unsupported || !
|
|
2189
|
+
if (!q.unsupported || !relayError) relayError = q.error;
|
|
2006
2190
|
}
|
|
2007
2191
|
if (!relay) {
|
|
2192
|
+
if (thin) {
|
|
2193
|
+
return fail(
|
|
2194
|
+
`${thin} ${ranked.length ? `Selling in its ${ranked[0].asset.symbol} market and swapping to ${out.symbol} through Relay did not work either: ${relayError}` : `No other market of this coin can pay for it either: ${firstError}`} Sell less, or receive another asset.`,
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2008
2197
|
const assets = await tradeAssets(c, coin);
|
|
2009
|
-
|
|
2198
|
+
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).`;
|
|
2199
|
+
return fail(`${relayError ?? firstError ?? none} This coin trades in ${assets.join(", ")}: receive ${assets.length > 1 ? "one of those" : "that"} instead.`);
|
|
2010
2200
|
}
|
|
2011
2201
|
const capped = await relayCapRefusal(c, leg, route.out);
|
|
2012
2202
|
if (capped) return fail(`The sale would pay ${capped}, more than one Relay swap converts. Receive ${leg.symbol} (receive "${leg.symbol}"), or sell less at a time.`);
|
|
@@ -2024,6 +2214,7 @@ server.registerTool(
|
|
|
2024
2214
|
expected: amountText(relay ? relay.out : route.out, out),
|
|
2025
2215
|
minimum: `${amountText(finalMin, out)}${relay ? " (estimated: Relay quotes again for what the sale returns)" : ""}`,
|
|
2026
2216
|
route: relay ? `${routeWords}, then ${relayText(relay)} (two transactions)` : routeWords,
|
|
2217
|
+
...(picked ? { market: picked } : {}),
|
|
2027
2218
|
slippageBps: Number(bps),
|
|
2028
2219
|
...(atFloor !== null && atFloor > route.out
|
|
2029
2220
|
? { betterAtFloor: `The Backed Floor pays more right now: redeem_floor would pay ${amountText(atFloor, leg)} for these coins.` }
|
|
@@ -2066,6 +2257,7 @@ server.registerTool(
|
|
|
2066
2257
|
received: proceeds !== null ? amountText(proceeds, out) : `about ${amountText(sent.result ?? route.out, out)}`,
|
|
2067
2258
|
minimum: quote.minimum,
|
|
2068
2259
|
route: quote.route,
|
|
2260
|
+
...(picked ? { market: picked } : {}),
|
|
2069
2261
|
tx: links(),
|
|
2070
2262
|
});
|
|
2071
2263
|
}
|
|
@@ -2471,7 +2663,7 @@ server.registerTool(
|
|
|
2471
2663
|
}
|
|
2472
2664
|
if (!coin) return reply({ ...info, tradable: false, note: "Classic bonding-curve coin: trade it on the site." });
|
|
2473
2665
|
info.tradeIn = await tradeAssets(c, coin);
|
|
2474
|
-
info.alsoTradeVia = "buy_token and sell_token also take ETH, USDG, kBTC
|
|
2666
|
+
info.alsoTradeVia = "buy_token and sell_token also take ETH, USDG, kBTC or a tokenized stock: Relay swaps it into one of tradeIn first (two transactions).";
|
|
2475
2667
|
const floor = await floorOf(coin).catch(() => null);
|
|
2476
2668
|
info.backedFloor = floor
|
|
2477
2669
|
? { floorPrice: `${floor.price} per ${coin.meta.symbol}`, reserve: amountText(floor.V, floor.quoteMeta), vault: floor.vault, redeem: "redeem_floor" }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hookit-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Let an AI agent launch a coin on hookit.fun (Ink) with its own hook modules, 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, and collect its creator fees.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "hookit-mcp": "index.js" },
|