add quote_sell, my_positions, pending_fees; make quotes keyless via canonical quoters

This commit is contained in:
Qian
2026-07-17 07:58:50 +00:00
parent 6ee1a5d0e7
commit 9aac4b1925
3 changed files with 86 additions and 6 deletions
+3 -1
View File
@@ -28,9 +28,11 @@ Omit `QIAN_KEY` for read-only mode (market data still works). Use a dedicated ho
| `get_token` | Full detail: price, FDV, hook, metadata |
| `get_trades` / `get_holders` | Market activity |
| `my_wallet` | Your address + ETH balance per chain |
| `quote_buy` | Simulate a buy (no gas) |
| `my_positions` | Holdings, ETH value, and PnL for any address across chains |
| `quote_buy` / `quote_sell` | Preview a trade, read-only (no key or approval) |
| `buy` / `sell` | Trade via QianSwapRouter (plain ETH in/out, auto-approve, slippage-protected) |
| `launch_token` | Deploy a token: name/symbol/image/socials, FDV tier 1–4, creator fee 0.5–20%, optional dev buy |
| `pending_fees` | Unclaimed creator/platform fees for a token (read-only) |
| `collect_fees` | Claim your 70% creator share of trading fees |
## Configuration
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qianexchange/mcp",
"version": "0.1.1",
"version": "0.1.2",
"description": "MCP server for the Qian multichain DEX & launchpad (qian.ag) — launch and trade tokens from any MCP-capable agent",
"type": "module",
"bin": { "qian-mcp": "server.js" },
+82 -4
View File
@@ -32,20 +32,27 @@ const CHAINS = {
chainId: 4663,
rpc: process.env.RPC_ROBINHOOD ?? 'https://rpc.mainnet.chain.robinhood.com',
explorer: 'https://robinhoodchain.blockscout.com',
quoterV2: '0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7',
v4Quoter: '0x8dc178efb8111bb0973dd9d722ebeff267c98f94',
},
base: {
...CORE,
chainId: 8453,
rpc: process.env.RPC_BASE ?? 'https://base-rpc.publicnode.com',
explorer: 'https://basescan.org',
quoterV2: '0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a',
v4Quoter: '0x0d5e0F971ED27FBfF6c2837bf31316121532048D',
},
ethereum: {
...CORE,
chainId: 1,
rpc: process.env.RPC_ETHEREUM ?? 'https://ethereum-rpc.publicnode.com',
explorer: 'https://etherscan.io',
quoterV2: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e',
v4Quoter: '0x52F0E24D1c21C8A0cB1e5a5dD6198556BD9E1203',
},
}
const V3_POOL_FEE = 10_000 // 1% tier (factory constant); ROUTER takes 1% off the WETH side each direction
const chainSlug = z.enum(['robinhood', 'base', 'ethereum'])
const address = z.string().regex(/^0x[0-9a-fA-F]{40}$/, 'EVM address')
const SUPPLY = 1_000_000_000n * 10n ** 18n
@@ -59,7 +66,10 @@ const ROUTER_ABI = [
'function buy(address token, uint256 minTokensOut) payable returns (uint256)',
'function sell(address token, uint256 tokensIn, uint256 minEthOut) returns (uint256)',
]
const FEE_ROUTER_ABI = ['function collectFees(address token)']
// collectFees returns the split it pays out; staticCall reads unclaimed fees without spending gas.
const FEE_ROUTER_ABI = ['function collectFees(address token) returns (uint256 creatorWeth, uint256 treasuryWeth)']
const QUOTER_V2_ABI = ['function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96) params) returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)']
const V4_QUOTER_ABI = ['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)']
const ERC20_ABI = [
'function balanceOf(address) view returns (uint256)',
'function allowance(address,address) view returns (uint256)',
@@ -100,6 +110,37 @@ function priceEthPerToken(x96, token, chain) {
return tokenIs0 ? p * p : 1 / (p * p)
}
// Read-only pool quote via the canonical Uniswap quoters, mirroring the frontend:
// 1% router fee off the WETH side (buys up front, sells on the way out). No key, no approval.
async function quoteOut(chain, tokenAddr, side, inWei) {
const C = CHAINS[chain]
const { token } = await api(`/tokens/${chain}/${tokenAddr}`)
const buy = side === 'buy'
const weth = WETH[chain]
const amtIn = buy ? (inWei * 99n) / 100n : inWei // buys: 1% ETH fee before the pool
let out
if (token.pool_kind === 'v3') {
const q = new ethers.Contract(C.quoterV2, QUOTER_V2_ABI, provider(chain))
const params = buy
? { tokenIn: weth, tokenOut: tokenAddr, amountIn: amtIn, fee: V3_POOL_FEE, sqrtPriceLimitX96: 0n }
: { tokenIn: tokenAddr, tokenOut: weth, amountIn: amtIn, fee: V3_POOL_FEE, sqrtPriceLimitX96: 0n }
out = (await q.quoteExactInputSingle.staticCall(params)).amountOut
} else {
const k = token.pool_key
if (!k) throw new Error(`v4 token ${tokenAddr} has no pool_key in the index yet`)
const wethIsC0 = k[0].toLowerCase() === weth
const q = new ethers.Contract(C.v4Quoter, V4_QUOTER_ABI, provider(chain))
const params = {
poolKey: { currency0: k[0], currency1: k[1], fee: k[2], tickSpacing: k[3], hooks: k[4] },
zeroForOne: buy ? wethIsC0 : !wethIsC0,
exactAmount: amtIn,
hookData: '0x',
}
out = (await q.quoteExactInputSingle.staticCall(params)).amountOut
}
return buy ? out : (out * 99n) / 100n // sells: 1% fee off the WETH out
}
const server = new McpServer({ name: 'qian', version: '0.1.0' })
server.registerTool('list_tokens', {
@@ -145,15 +186,43 @@ server.registerTool('my_wallet', {
return json({ address: w.address, eth: balances })
})
server.registerTool('my_positions', {
description: 'Token holdings for an address across all chains, with current ETH value and PnL. Defaults to your own wallet (from QIAN_KEY); pass an address to inspect any wallet (read-only, no key needed).',
inputSchema: { address: address.optional().describe('wallet to inspect; defaults to your QIAN_KEY address') },
}, async ({ address: addr }) => {
const who = addr ?? (process.env.QIAN_KEY ? new ethers.Wallet(process.env.QIAN_KEY).address : null)
if (!who) throw new Error('no address — pass one or set QIAN_KEY')
const { positions } = await api(`/profile/${who}`)
const rows = positions.map((p) => {
const priceEth = p.last_price_x96 ? priceEthPerToken(p.last_price_x96, p.token, p.chain) : null
const balance = Number(p.balance) / 1e18
const valueEth = priceEth === null ? null : balance * priceEth
const pnlEth = Number(p.sell_eth) / 1e18 + (valueEth ?? 0) - Number(p.buy_eth) / 1e18
return {
chain: p.chain, address: p.token, symbol: p.symbol, balance: String(balance),
price_eth: priceEth, value_eth: valueEth, pnl_eth: pnlEth,
url: `https://qian.ag/${p.chain}/token/${p.token}`,
}
})
return json({ address: who, positions: rows })
})
server.registerTool('quote_buy', {
description: 'Simulate buying a token with ETH via QianSwapRouter (1% router fee included). Returns expected tokens out. Your wallet must hold at least eth_amount for the simulation.',
description: 'Preview buying a token with ETH via QianSwapRouter (1% router fee included). Read-only — no wallet or key needed. Returns expected tokens out.',
inputSchema: { chain: chainSlug, address, eth_amount: z.string().describe('ETH to spend, e.g. "0.05"') },
}, async ({ chain, address: addr, eth_amount }) => {
const router = new ethers.Contract(CHAINS[chain].swapRouter, ROUTER_ABI, wallet(chain))
const out = await router.buy.staticCall(addr, 0n, { value: ethers.parseEther(eth_amount) })
const out = await quoteOut(chain, addr, 'buy', ethers.parseEther(eth_amount))
return json({ eth_in: eth_amount, tokens_out: ethers.formatEther(out), pct_of_supply: Number((out * 10000n) / SUPPLY) / 100 })
})
server.registerTool('quote_sell', {
description: 'Preview selling a token for ETH via QianSwapRouter (1% router fee included). Read-only — no wallet, key, or approval needed. Returns expected ETH out.',
inputSchema: { chain: chainSlug, address, token_amount: z.string().describe('Tokens to sell in whole tokens, e.g. "1000000"') },
}, async ({ chain, address: addr, token_amount }) => {
const out = await quoteOut(chain, addr, 'sell', ethers.parseEther(token_amount))
return json({ token_in: token_amount, eth_out: ethers.formatEther(out) })
})
async function sendAndWait(txPromise, chain, label) {
const tx = await txPromise
const rc = await tx.wait()
@@ -254,6 +323,15 @@ server.registerTool('launch_token', {
return json({ token, hash, explorer, page: `https://qian.ag/${p.chain}/token/${token}`, metadata_uri: uri, deploy_fee_eth: ethers.formatEther(fee) })
})
server.registerTool('pending_fees', {
description: 'Unclaimed trading fees for a token, in WETH. Read-only (simulates the permissionless collect). Split is 70% creator / 30% platform; call collect_fees to pay it out.',
inputSchema: { chain: chainSlug, address },
}, async ({ chain, address: addr }) => {
const feeRouter = new ethers.Contract(CHAINS[chain].feeRouter, FEE_ROUTER_ABI, provider(chain))
const [creatorWeth, treasuryWeth] = await feeRouter.collectFees.staticCall(addr, { from: ethers.ZeroAddress })
return json({ creator_weth: ethers.formatEther(creatorWeth), platform_weth: ethers.formatEther(treasuryWeth) })
})
server.registerTool('collect_fees', {
description: 'Collect accrued trading fees for a token via QianFeeRouter (permissionless — anyone can trigger; payout always goes 70% to the token creator, 30% to the platform).',
inputSchema: { chain: chainSlug, address },