Subchapter 2.16
plugins/hydrex.mdMarkdown19 KBView on GitHub
[!IMPORTANT] Run Base MCP onboarding first (see SKILL.md). Obtain the user’s wallet address via
get_wallets— it is required asfromorrecipientin every prepare call. The Hydrex prepare server must be running locally before any write operations.
Hydrex is an Omni-Liquidity MetaDEX on Base — concentrated-liquidity swaps aggregated across 0x, OpenOcean, OKX, and KyberSwap, plus liquidity positions that earn fees and rewards automatically. Adding liquidity creates an active earning position immediately; there is no separate staking step. The plugin calls a local prepare server (https://hydrex-agent.com) to fetch unsigned calldata, then submits via send_calls on Base mainnet (chainId: 8453).
| Capability | Harness surface (Cursor, Claude Code, Codex) | Chat-only surface (Claude.ai, ChatGPT) |
|---|---|---|
| Read state (quote, positions, portfolio, pools) | Harness HTTP tool → GET https://hydrex-agent.com/state/* | User-paste fallback: construct full URL, ask user to open in browser and paste JSON response |
| Prepare calldata (swap, add/remove liquidity) | Harness HTTP tool → GET https://hydrex-agent.com/prepare/* | User-paste fallback: same as above |
| Submit transaction | send_calls (Base MCP) | send_calls (Base MCP) |
The prepare server (https://hydrex-agent.com) is not on the Base MCP web_request allowlist. On chat-only surfaces, construct the full GET URL with all query parameters and ask the user to open it in a browser, paste the JSON response into chat, then continue with send_calls.
Server URL: https://hydrex-agent.com
Response: { "ok": true, "service": "hydrex-base-skill-server", "chainId": 8453 }
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenIn | address | ✓ | Input token contract address |
tokenOut | address | ✓ | Output token contract address |
amount | string (wei) | ✓ | Input amount in raw units |
recipient | address | ✓ | Wallet that receives output tokens |
slippage | number | — | Slippage tolerance in bps (default: 50 = 0.5%) |
source | string | — | Force aggregator: ZEROX, OPENOCEAN, OKX, KYBERSWAP |
Response:
{
"ok": true,
"data": {
"tokenIn": "0x...", "tokenOut": "0x...",
"amountIn": "1000000", "amountOut": "412345678901234",
"source": "ZEROX", "priceImpact": "0.12",
"to": "0x...", "data": "0x...", "value": "0x0"
}
}Always show amountOut (human-readable) and priceImpact to the user before executing. Warn and require confirmation if priceImpact > 5%.
GET /state/portfolio?address=<walletAddress>Returns token balances and LP positions for the wallet.
GET /state/positions?address=<walletAddress>Returns all open concentrated liquidity positions owned by the wallet (read from NonfungiblePositionManager on-chain).
Response shape:
{
"ok": true,
"count": 2,
"positions": [
{
"positionId": "12345",
"token0": "0x...", "token1": "0x...",
"fee": 500, "tickLower": -887220, "tickUpper": 887220,
"liquidity": "1500000000000000",
"tokensOwed0": "0", "tokensOwed1": "0"
}
]
}Use positionId with /prepare/remove-liquidity.
GET /state/trade-history?address=<walletAddress>Returns past swaps executed through Hydrex for the wallet.
Discovers Hydrex pools — including each pool’s contract address, token addresses, and decimals — from the Hydrex stats API. This is how the agent resolves a pool address (and token0/token1/decimals0/decimals1) for /prepare/add-liquidity and /prepare/remove-liquidity when the user only gives a token pair.
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenA | string | — | Token symbol (e.g. USDC) or address; matches either side of the pair (order-independent) |
tokenB | string | — | Second token symbol or address, for unordered pair matching |
token0 | string | — | Order-specific token0 symbol or address |
token1 | string | — | Order-specific token1 symbol or address |
liquidityType | string | — | Filter by pool type: integral, classic-volatile, classic-stable |
days | number | — | Stats lookback window in days, 1–30 (default: 1) |
limit | number | — | Max upstream pairs to scan, 1–1000 (default: 1000) |
Pass tokenA + tokenB to find pools for a specific pair regardless of token order. With no token filters it returns every pool. Results are sorted by TVL (USD) descending, so pools[0] is the deepest pool for the pair.
Response:
{
"ok": true,
"count": 1,
"summary": { },
"pools": [
{
"id": "0x<poolAddress>",
"pool": "0x<poolAddress>",
"liquidityType": "integral",
"fee": "500",
"token0": { "address": "0x...", "symbol": "USDC", "name": "USD Coin", "decimals": 6 },
"token1": { "address": "0x...", "symbol": "WETH", "name": "Wrapped Ether", "decimals": 18 },
"tvlUsd": "1234567.89",
"volumeUsd": "...", "feesUsd": "...", "averageFee": "..."
}
]
}Each entry gives the pool address plus token0/token1 addresses and decimals — exactly the inputs /prepare/add-liquidity needs (use them directly, preserving the returned token0/token1 order). Handling:
count = 0 — no pool exists for that pair; tell the user and do not call a prepare endpoint.count > 1 — multiple pools/fee tiers; show the options (tvlUsd, fee, liquidityType) and let the user choose before proceeding.count = 1 — use it; confirm with the user before any write.Never guess or fabricate a pool address — discover it here, or ask the user (addresses are also listed on the Hydrex app at https://hydrex.fi (opens in a new tab)).
| Parameter | Type | Required | Description |
|---|---|---|---|
tokenIn | address | ✓ | Input token address |
tokenOut | address | ✓ | Output token address |
amount | string | ✓ | Human-readable input amount (e.g. "1.5") |
decimals | number | — | Decimals of tokenIn (default: 18) |
recipient | address | ✓ | Wallet that receives output tokens |
slippage | number | — | Slippage in bps (default: 50) |
source | string | — | Optional aggregator override |
Example:
GET /prepare/swap?tokenIn=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&tokenOut=0x4200000000000000000000000000000000000006&amount=1.5&decimals=6&recipient=0xYourWallet&slippage=50Response:
{
"ok": true,
"quote": {
"tokenIn": "0x...", "tokenOut": "0x...",
"amountIn": "1500000", "amountOut": "618522345678901",
"source": "ZEROX", "priceImpact": "0.08"
},
"transactions": [
{ "step": "approve-tokenIn", "to": "0x<tokenIn>", "data": "0x...", "value": "0x0", "chainId": 8453 },
{ "step": "swap", "to": "0x<SwapRouter>", "data": "0x<calldata>", "value": "0x0", "chainId": 8453 }
]
}The transactions[] array length is not fixed. For an ERC-20 tokenIn, an approve-tokenIn step is prepended only when the wallet’s current allowance is too low — if allowance is already sufficient, just the swap step is returned. Native ETH swaps never include an approve step. Always pass every returned transaction to send_calls in order, rather than assuming a single call.
| Parameter | Type | Required | Description |
|---|---|---|---|
from | address | ✓ | Wallet providing liquidity |
pool | address | ✓ | Pool contract address — resolve from the token pair via GET /state/pools, or ask the user (also listed on https://hydrex.fi (opens in a new tab)) |
token0 | address | ✓ | token0 address (must match pool order) |
token1 | address | ✓ | token1 address (must match pool order) |
decimals0 | number | — | token0 decimals (default: 18) |
decimals1 | number | — | token1 decimals (default: 18) |
amount0 | string | ✓ | Desired token0 amount, human-readable |
amount1 | string | ✓ | Desired token1 amount, human-readable |
rangePreset | string | — | Named range around current price: tight (±1%), narrow (±5%), common (±12.5%), wide (±20%). Used when no other range option is given (default: common) |
priceLower | number | — | Custom lower price bound (token1/token0). Must be sent together with priceUpper |
priceUpper | number | — | Custom upper price bound (token1/token0). Must be sent together with priceLower |
fullRange | boolean | — | If true, spreads liquidity across the entire price range (always-active, V2-style). Default false |
slippage | number | — | Slippage in bps (default: 50) |
The range is set in exactly one of these mutually exclusive ways — rangePreset, a custom priceLower+priceUpper, or fullRange. Sending more than one returns 400 (“Choose only one range option…”); sending only one of priceLower/priceUpper returns 400 (“A custom range requires both…”). If you send none, the server uses the common preset (±12.5%).
Response — three transactions, always in this order:
{
"ok": true,
"position": { "tickLower": -887220, "tickUpper": 887220, "amount0": "0.05", "amount1": "100.0" },
"transactions": [
{ "step": "approve-token0", "to": "0x<token0>", "data": "0x...", "value": "0x0", "chainId": 8453 },
{ "step": "approve-token1", "to": "0x<token1>", "data": "0x...", "value": "0x0", "chainId": 8453 },
{ "step": "mint", "to": "0x<NFPM>", "data": "0x...", "value": "0x0", "chainId": 8453 }
]
}Choose one of three mutually exclusive ways to set the range (the server does the percentage→price math on-chain, so the agent only passes the choice):
| Option | How to send it | Resulting range |
|---|---|---|
| Named preset | rangePreset=tight | ±1% of current price |
| Named preset | rangePreset=narrow | ±5% |
| Named preset | rangePreset=common (default) | ±12.5% |
| Named preset | rangePreset=wide | ±20% |
| Custom | priceLower=<p>&priceUpper=<p> | exact bounds you supply |
| Full range | fullRange=true | entire price range (always-active) |
If the user gives no preference, the server applies the common preset (±12.5%). Always tell the user which range was used, e.g. “I’m using the common ±12.5% range around the current price — say the word for a tighter (narrow/tight), wider (wide), full, or custom range.”
| Parameter | Type | Required | Description |
|---|---|---|---|
from | address | ✓ | Wallet that owns the position |
positionId | number | ✓ | NFT tokenId from /state/positions |
pool | address | ✓ | Pool contract address — the matching position from /state/positions identifies the pair (token0/token1/fee); resolve the address via GET /state/pools (or ask the user) |
decimals0 | number | — | token0 decimals (default: 18) |
decimals1 | number | — | token1 decimals (default: 18) |
liquidityPercent | number | — | Percentage to remove, 1–100 (default: 100) |
slippage | number | — | Slippage in bps (default: 50) |
Response:
{
"ok": true,
"transactions": [
{ "step": "remove-liquidity", "to": "0x<NFPM>", "data": "0x...", "value": "0x0", "chainId": 8453 }
]
}All prepare endpoints return { "ok": false, "error": "..." } on failure — surface the error field to the user and do not call send_calls.
1. get_wallets → address
2. GET /state/quote?tokenIn=...&tokenOut=...&amount=...&recipient=<address>
→ show amountOut (human-readable) and priceImpact
→ if priceImpact > 5%, warn user and require confirmation
3. GET /prepare/swap?tokenIn=...&tokenOut=...&amount=...&recipient=<address>
→ transactions[]
4. send_calls(chain="base", calls=[{to, value, data} for each tx])
5. get_request_status(requestId) — poll automatically until success or failed
→ report outcome; do NOT ask user to type anything1. get_wallets → address
2. GET /state/positions?address=<address> → show existing positions for context
→ if count = 0 (no positions yet), that's fine — this will be the user's first
position; continue without prompting for an existing one
3. Resolve the pool: GET /state/pools?tokenA=<t0>&tokenB=<t1>
→ count = 0: no pool exists, tell the user and stop
→ count > 1: show options (tvlUsd, fee, liquidityType) and let the user pick
→ use the chosen pool's pool/token0/token1/decimals; never guess an address
4. Confirm: pool address, token pair, amounts, range (preset/custom/full; default `common` ±12.5%)
5. GET /prepare/add-liquidity?from=<address>&pool=<pool>&token0=<t0>&token1=<t1>
&decimals0=<d0>&decimals1=<d1>&amount0=<a0>&amount1=<a1>
[ one range option only: &rangePreset=<tight|narrow|common|wide>
| &priceLower=<p>&priceUpper=<p> | &fullRange=true ]
→ show position.tickLower, tickUpper, amount0, amount1 to user before proceeding
6. send_calls(chain="base", calls from transactions[])
7. get_request_status(requestId) — poll automatically until success or failed1. get_wallets → address
2. GET /state/positions?address=<address> → list positionId values
→ if count = 0 (no open positions), tell the user they have nothing to remove
and stop — do not call /prepare/remove-liquidity
3. Confirm which positionId and what percentage to remove (default: 100%)
→ resolve the pool address with GET /state/pools?tokenA=<position.token0>&tokenB=<position.token1>
(match on the position's fee tier when count > 1)
4. GET /prepare/remove-liquidity?from=<address>&positionId=<id>&pool=<pool>
&decimals0=<d0>&decimals1=<d1>[&liquidityPercent=<pct>]
→ transactions[]
5. send_calls(chain="base", calls from transactions[])
6. get_request_status(requestId) — poll automatically until success or failedTarget tool: send_calls
Map every transactions[] array from a prepare endpoint into send_calls:
{
"chain": "base",
"calls": [
{ "to": "<tx.to>", "value": "<tx.value>", "data": "<tx.data>" }
]
}Pass all transactions in a single calls array — Base MCP executes them atomically in one user approval. After send_calls returns, immediately call get_request_status(requestId) and poll until the status is success or failed. Do not ask the user to type or paste anything during polling. See approval-mode.md.
“Swap 5 USDC for ETH on Hydrex”
get_wallets → wallet addressGET /state/quote?tokenIn=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&tokenOut=0x4200000000000000000000000000000000000006&amount=5000000&recipient=<address> → show amountOut, priceImpactGET /prepare/swap?tokenIn=0x833589...&tokenOut=0x420000...&amount=5&decimals=6&recipient=<address>&slippage=50send_calls(chain="base", calls=[swap tx])get_request_status → report outcome“Show my Hydrex liquidity positions”
get_wallets → wallet addressGET /state/positions?address=<address> → display each positionId, token pair, tick range, and liquidity“Add liquidity to the USDC/ETH pool on Hydrex — 100 USDC and 0.04 ETH”
get_wallets → wallet addressGET /state/positions?address=<address> → existing position context (count may be 0 — fine for a first position)GET /state/pools?tokenA=USDC&tokenB=WETH → pick the pool (if count > 1, let the user choose); read its pool/token0/token1/decimals. Range defaults to the common preset (±12.5%) unless the user picks another; inform the user of the range usedGET /prepare/add-liquidity?from=<address>&pool=<pool>&token0=<USDC>&token1=<WETH>&decimals0=6&decimals1=18&amount0=100&amount1=0.04position (tickLower, tickUpper, amounts) to usersend_calls(chain="base", calls=[approve-token0, approve-token1, mint])get_request_status → report outcome“Remove 50% of liquidity from Hydrex position #12345” (chat-only surface fallback)
get_wallets → wallet addressweb_request cannot reach https://hydrex-agent.com → construct the full URL and ask the user to open it in a browser and paste the JSON response into chatsend_calls(chain="base", calls from transactions[])get_request_status → report outcomepriceImpact before executing; if priceImpact > 5%, warn the user and wait for explicit confirmation. Never auto-raise slippage.send_calls.| Symbol | Address | Decimals |
|---|---|---|
| USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | 6 |
| WETH | 0x4200000000000000000000000000000000000006 | 18 |
| ETH (native) | Use the value field; no tokenIn address needed | 18 |
For other tokens, look up the address (and decimals) via GET /state/pools (returns token0/token1 with address, symbol, and decimals), or ask the user to supply the contract address.
| Condition | Action |
|---|---|
get_wallets returns no wallet | Tell user to connect their Base Account and retry |
/state/positions returns count: 0 | For add-liquidity, proceed — this is the user’s first position. For remove-liquidity, tell the user they have no open positions and stop |
| Pool address unknown | Resolve via GET /state/pools (by token pair); if it returns count: 0, no pool exists — tell the user and stop. Never guess an address |
/state/pools returns count > 1 | Multiple pools/fee tiers for the pair; show tvlUsd/fee/liquidityType and let the user choose |
/state/quote returns priceImpact > 5% | Warn user; require confirmation before proceeding |
Prepare endpoint returns ok: false | Surface the error field; do not call send_calls |
send_calls approval rejected | Inform user the transaction was cancelled; offer to retry |
get_request_status shows failure | Parse the failure reason and suggest next steps |
positionId is the NFT tokenId from the NonfungiblePositionManager; always fetch current positions via /state/positions before a remove.