
Meta Dex Aggregator
- 9 installs
- 1 repo stars
- Updated July 29, 2026
- starchild-ai-agent/community-skills
Helps with ai & agent building tasks during AI-assisted development.
About
meta-dex-aggregator is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- meta-dex-aggregator
- AI & Agent Building
- AI-coding skill
Meta Dex Aggregator by the numbers
- 9 all-time installs (skills.sh)
- Ranked #12,152 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/starchild-ai-agent/community-skills --skill meta-dex-aggregatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 9 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 29, 2026 |
| Repository | starchild-ai-agent/community-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Meta DEX Aggregator — Multi-Source Quote Comparison with Safety Layer
v3.0.0 — Now with built-in execution, CowSwap polling, historical logging, and analytics.
Aggregator of aggregators. Queries 6 DEX aggregators in parallel, ranks by gas-adjusted net output, and runs safety checks before execution.
New in v3.1.0
| Feature | Description |
|---|---|
| Market orders | --market-order uses 1inch for instant execution (no limit order failures) |
| Auto-verify | --auto-verify fetches balances automatically via RPC |
| Retry logic | Fallback to next best aggregator on failure |
| Better gas estimation | Calibrated per aggregator (150k-300k vs flat 580k) |
| Price freshness check | Warns if quote is >30s old before execution |
New in v3.0.0
| Feature | Description |
|---|---|
| Built-in execution | execute command handles swap + verification in one flow |
| CowSwap polling | Auto-poll CowSwap orders until fulfilled (up to 2 min) |
| Historical logging | Every quote logged to JSONL for trend analysis |
| Winner analytics | stats command shows which aggregator wins most often |
| Price trends | trend command shows net output over time |
| Slippage analysis | slippage command analyzes competitive spreads |
| CSV export | export command for external analysis |
| Quote monitoring | monitor command alerts when target net output is reached |
Safety Features
1. Price Impact — Fetches fair market price from DefiLlama coins API (+ DexScreener fallback), compares vs quote output. Thresholds: 3% warning, 5% high, 10% critical (blocks swap). 2. Gas-Adjusted Ranking — netOut = amountUsd - gasUsd. Best route ≠ most tokens. 3. MEV Protection Flags — CowSwap and 0x Gasless are flagged isMEVSafe. Recommends MEV-safe route when within 0.5% of best price. 4. Slippage Warnings — Sandwich risk >1%, stablecoin pairs >0.05%, too-low revert risk. 5. Outlier Detection — Quotes >5% worse than best are flagged as outliers. 6. Post-Swap Verification — Mandatory balance checks, flags >2% deviation.
Aggregators
| Adapter | API Key | Status | MEV-Safe |
|---|---|---|---|
| ParaSwap | None needed | ✅ | ❌ |
| Odos | None needed | ✅ | ❌ |
| KyberSwap | None needed | ✅ | ❌ |
| CowSwap | None needed | ✅ | ✅ |
| 1inch | Native tool (platform-proxied) | ✅ | ❌ |
| Matcha/0x | OX_API_KEY in .env | ✅ | ❌ |
CowSwap: Batch auction protocol — solvers compete off-chain, user never exposed to MEV. Gasless for the user (solvers pay). Supported on Ethereum, Arbitrum, Gnosis, Base. Uses wrapped native tokens (WETH) internally — raw ETH is auto-converted. Execution is order-based (EIP-712 signed intent), not raw transaction.
Workflow: Quote with Safety Check
Step 1 & 2 run in PARALLEL (no dependency between them):
# 1. Get 5 aggregator quotes (ParaSwap, Odos, KyberSwap, CowSwap, Matcha/0x + safety)
cd skills/meta-dex-aggregator/scripts && \
python3 meta_dex.py quote --chain base --from ETH --to USDC --amount 0.5 --slippage 0.5# 2. Get 1inch quote via native tool (proxied, no API key needed)
oneinch_quote(chain="base", src="<from_addr>", dst="<to_addr>", amount="<amount_in_wei>")
# Returns: { "dstAmount": "<raw_amount>" }
# Convert: dstAmount / 10^decimals = human amountStep 3 — Merge & present:
- Script returns quotes with:
aggregator,amountOutHuman,amountUsd,gasUsd,netOut,vsbestPct - 1inch native tool returns
dstAmount(raw wei) — convert to human amount using token decimals - Insert 1inch into the ranked table, recalculate
vsbestPctif 1inch is the new winner - The script's
safetyblock (priceImpact, slippageWarnings, recommendation) applies to all quotes - If
priceImpact.severityis "high"/"critical" — WARN and block the swap - Execute via
oneinch_swapif 1inch wins, orwallet_transferwith tx data for others
Workflow: Execute Swap (v3.1.0 — Market Orders + Auto-Verify)
NEW in v3.1.0:
--market-orderflag for instant 1inch execution (no limit order failures)--auto-verifyflag for automatic balance fetching (no manual args)- Retry logic with fallback aggregators on failure
Option A: Market Order (Recommended for < $100 swaps)
# Instant execution via 1inch market order
cd skills/meta-dex-aggregator/scripts && \
python3 meta_dex.py execute --chain arbitrum --from ETH --to USDC --amount 0.005 \
--market-order --wallet 0x... --slippage 2.0
# Response:
# {
# "step": "market_order_ready",
# "mode": "market_order",
# "instruction": "Execute via oneinch_swap(chain='arbitrum', src='0x...', dst='0x...', amount='...', slippage=2.0)"
# }
# Agent executes:
oneinch_swap(chain="arbitrum", src="0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", dst="0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", amount="5000000000000000", slippage=2.0)
# Auto-verify after execution:
python3 meta_dex.py execute --chain arbitrum --from ETH --to USDC --amount 0.005 \
--market-order --wallet 0x... --auto-verify --expected-out <actual_received_wei>Option B: Limit Order with Auto-Verification
# Get quote and execute
python3 meta_dex.py execute --chain arbitrum --from ETH --to USDC --amount 0.005 \
--aggregator kyberswap --wallet 0x... --slippage 1.0
# Execute tx via wallet_transfer...
# Auto-verify (no manual balance args needed):
python3 meta_dex.py execute --chain arbitrum --from ETH --to USDC --amount 0.005 \
--aggregator kyberswap --wallet 0x... --verify --auto-verify --expected-out 10770000Option B: Manual swap (legacy)
# Get swap tx data
python3 meta_dex.py swap --chain base --from ETH --to USDC --amount 0.5 \
--aggregator odos --wallet 0x... --slippage 0.5
# Execute and verify manually (see Post-Swap Verification section)Post-Swap Verification (MANDATORY)
v3.0.0: Use the `execute --verify` command for automatic verification.
Manual verification workflow (if not using execute command):
# 1. Record balances BEFORE the swap
wallet_balance(chain="base", asset="eth") → pre_from_balance
wallet_balance(chain="base", asset="usdc") → pre_to_balance
# 2. Execute the swap (wallet_transfer or oneinch_swap)
# 3. For CowSwap: poll until fulfilled
# Use cowswap_poll_order(chain, order_uid) function
# Poll every 5s for up to 180s
# 4. Verify balances AFTER the swap
wallet_balance(chain="base", asset="eth") → post_from_balance
wallet_balance(chain="base", asset="usdc") → post_to_balance
# 5. Verify with CLI
python3 meta_dex.py execute --verify \
--chain base --from ETH --to USDC --amount 0.5 --aggregator kyberswap \
--wallet 0x... \
--pre-from-balance <pre_eth> --pre-to-balance <pre_usdc> \
--post-from-balance <post_eth> --post-to-balance <post_usdc> \
--expected-out <expected_out_wei>
# Returns: {"verification": "PASSED"|"FAILED", "deviationPct": 0.5, ...}Rules:
- Never report success without checking post-swap balances — tx confirmed ≠ expected outcome
- Compare actual received vs quoted amount — if deviation >2%, flag it to the user
- For CowSwap orders: Orders are filled asynchronously by solvers (can take up to 2 minutes).
Use cowswap_poll_order(chain, order_uid) to poll until status == "fulfilled".
- For 1inch via oneinch_swap: The tool returns a tx hash. Wait for confirmation, then check balances.
- Include in confirmation: tx hash, actual amounts spent/received, price achieved (received/spent)
Cross-Chain Swaps
When src_chain ≠ dst_chain, use xquote to compare cross-chain routes:
python3 skills/meta-dex-aggregator/scripts/meta_dex.py xquote \
--src-chain arbitrum --dst-chain polygon \
--from ETH --to USDC --amount 0.5 \
--wallet $WALLET --slippage 3Cross-chain sources:
- LI.FI — aggregates 20+ bridges (Relay, Stargate, Across, Hop, etc.). Returns ready-to-sign tx data. Execute via
wallet_transfer(to, amount=value, data, chain_id, gas_limit). Track status withcurl -s "https://li.quest/v1/status?txHash={hash}&fromChain={src_id}&toChain={dst_id}". - 1inch Fusion+ — intent-based atomic swaps (gasless, resolver handles both chains). The script returns a
needsToolCall: truemarker. Complete withoneinch_cross_chain_quotetool for the quote, thenoneinch_cross_chain_swapfor execution.
Cross-chain workflow: 1. Run xquote → get LI.FI routes + 1inch Fusion+ marker 2. Call oneinch_cross_chain_quote tool to fill in the 1inch quote 3. Compare all routes: output amount, total fees (gas + bridge + protocol), estimated time 4. Present table to user with clear winner 5. For execution: LI.FI routes → wallet_transfer with tx data; 1inch → oneinch_cross_chain_swap 6. Cross-chain is non-atomic — track status and confirm delivery on destination chain
Safety notes for cross-chain:
- Default slippage 3% (bridges need more than same-chain swaps)
- Always show estimated delivery time (4s to 10min depending on bridge)
- Always show fee breakdown (gas + bridge fees + protocol fees)
- After execution, track until funds arrive on destination chain
- LI.FI
DONE/PARTIALmeans bridge delivered but not the final token — may need a manual swap
Historical Quote Logging (v3.0.0)
Every quote is automatically logged to skills/meta-dex-aggregator/logs/{chain}_{FROM}_{TO}.jsonl.
What's logged:
- Timestamp, chain, tokens, amount
- All aggregator quotes (amount, gas, netOut, vsBest)
- Winner aggregator
- Net output in USD
Log retention: Unlimited (append-only JSONL). Manually prune old logs if needed.
Analytics Commands (v3.0.0)
Winner Statistics
python3 meta_dex.py stats --chain arbitrum --from ETH --to USDC --days 7Shows which aggregator wins most often, win rates, and average net output per aggregator.
Price Trends
python3 meta_dex.py trend --chain arbitrum --from ETH --to USDC --days 7 --bucket-hours 4Shows net output over time in 4-hour buckets (avg, min, max, count).
Slippage Analysis
python3 meta_dex.py slippage --chain arbitrum --from ETH --to USDC --days 7Analyzes competitive spreads between top 2 aggregators. Low spread = highly competitive.
CSV Export
python3 meta_dex.py export --chain arbitrum --from ETH --to USDC --days 30 --output /tmp/quotes.csvExports all quotes to CSV for external analysis (Excel, Python, etc.).
Quote Monitoring (v3.0.0)
Monitor for a target net output and alert when reached:
python3 meta_dex.py monitor --chain arbitrum --from ETH --to USDC --amount 1.0 \
--target-net-out 2050 --interval 60 --max-runs 10- Polls every 60 seconds
- Stops when net output ≥ $2050
- Max 10 polls (omit for unlimited)
- Returns immediately when target is met
Use case: "Alert me when ETH→USDC on Arbitrum nets >$2050 after gas"
Run this as a background task with sessions_spawn for non-blocking monitoring.
Token Resolution — Smart Confirmation
Token resolution is tiered to avoid bothering the user for obvious tokens while protecting against picking the wrong contract for ambiguous ones.
Confidence levels returned by resolve_token:
| Confidence | Meaning | Action |
|---|---|---|
trusted | Hardcoded canonical address (USDC, WETH, WBTC, etc.) | ✅ Auto-use, no confirmation |
exact | User provided a 0x address directly | ✅ Auto-use, no confirmation |
single | Only one token matches the symbol on this chain | ✅ Auto-use, no confirmation |
high | Multiple matches but top has >10x volume of runner-up | ✅ Auto-use, no confirmation |
ambiguous | Multiple plausible matches, none dominant | ⚠️ MUST confirm with user |
When confidence == "ambiguous":
The result includes a candidates list. Present them to the user:
I found multiple tokens matching "XYZ" on Arbitrum:
1. XYZ (XYZ Protocol) — 0x1234...5678 — $2.3M 24h vol
2. XYZ (XYZ Finance) — 0xabcd...ef01 — $180K 24h vol
3. XYZ (Old XYZ) — 0x9876...5432 — $12K 24h vol
Which one did you mean? (or paste the contract address directly)Once the user picks, re-call with the address directly to bypass resolution.
Trusted token coverage:
Ethereum, Arbitrum, Base, Optimism, Polygon, BSC, Avalanche, Gnosis — all major tokens (WETH, USDC, USDT, DAI, WBTC, LINK, UNI, AAVE, stETH, plus chain-specific tokens like ARB, OP, AERO, GMX, etc.)
Chain Support
ethereum, bsc, polygon, optimism, arbitrum, avalanche, gnosis, fantom, zksync, base, linea, scroll, sonic, unichain
Safety Response Format
The quote command returns a safety block:
{
"recommendation": "✅ All checks passed. Best route looks safe.",
"priceImpact": {"value": 0.02, "severity": "ok"},
"slippageWarnings": [],
"marketPrices": {"gas_token_price": 2170, "from_token_price": 2170, "to_token_price": 1.0}
}Severity levels: ok | warning (3%) | high (5%) | critical (10%)
CRITICAL: If severity is "high" or "critical", BLOCK the swap and warn the user explicitly.
"""DEX aggregator adapters for Meta DEX Aggregator."""
import os, requests
from chains import (CHAINS, ZERO_ADDR, NATIVE_PLACEHOLDER, DEFILLAMA_REFERRER)
# ── ParaSwap ──────────────────────────────────────────────────────────────────
PARASWAP_CHAINS = {"ethereum","bsc","polygon","avax","arbitrum","fantom","optimism","base","gnosis","sonic","unichain"}
def paraswap_quote(chain, chain_id, from_tok, to_tok, amount_wei, wallet, slippage):
if chain not in PARASWAP_CHAINS:
return None
from_addr = NATIVE_PLACEHOLDER if from_tok["address"] == ZERO_ADDR else from_tok["address"]
to_addr = NATIVE_PLACEHOLDER if to_tok["address"] == ZERO_ADDR else to_tok["address"]
url = (
f"https://apiv5.paraswap.io/prices/?srcToken={from_addr}&destToken={to_addr}"
f"&amount={amount_wei}&srcDecimals={from_tok['decimals']}&destDecimals={to_tok['decimals']}"
f"&partner=llamaswap&side=SELL&network={chain_id}"
f"&excludeDEXS=ParaSwapPool,ParaSwapLimitOrders&version=6.2"
)
data = requests.get(url, timeout=15).json()
if "error" in data:
return None
result = {
"aggregator": "ParaSwap",
"amountOut": data["priceRoute"]["destAmount"],
"amountIn": data["priceRoute"]["srcAmount"],
"gas": data["priceRoute"].get("gasCost", "0"),
"tokenApprovalAddress": data["priceRoute"].get("tokenTransferProxy"),
}
if wallet and wallet != ZERO_ADDR:
slippage_bps = int(float(slippage) * 100) if slippage else 100
tx_resp = requests.post(
f"https://apiv5.paraswap.io/transactions/{chain_id}?ignoreChecks=true",
json={
"srcToken": data["priceRoute"]["srcToken"],
"srcDecimals": data["priceRoute"]["srcDecimals"],
"destToken": data["priceRoute"]["destToken"],
"destDecimals": data["priceRoute"]["destDecimals"],
"slippage": slippage_bps,
"userAddress": wallet,
"partner": "llamaswap",
"partnerAddress": DEFILLAMA_REFERRER,
"takeSurplus": True,
"priceRoute": data["priceRoute"],
"isCapSurplus": True,
"srcAmount": data["priceRoute"]["srcAmount"],
},
headers={"Content-Type": "application/json"},
timeout=15,
)
tx_data = tx_resp.json()
if "error" not in tx_data:
result["tx"] = {
"to": tx_data["to"], "data": tx_data["data"],
"value": tx_data.get("value", "0"),
"gas": tx_data.get("gas", data["priceRoute"].get("gasCost", "0")),
"from": tx_data["from"],
}
return result
# ── Odos ──────────────────────────────────────────────────────────────────────
ODOS_CHAINS = {"ethereum","arbitrum","optimism","base","polygon","avax","bsc","fantom","zksync","linea","scroll","sonic","unichain"}
ODOS_ROUTERS = {
"ethereum": "0xcf5540fffcdc3d510b18bfca6d2b9987b0772559",
"arbitrum": "0xa669e7a0d4b3e4fa48af2de86bd4cd7126be4e13",
"optimism": "0xca423977156bb05b13a2ba3b76bc5419e2fe9680",
"base": "0x19ceead7105607cd444f5ad10dd51356436095a1",
"polygon": "0x4e3288c9ca110bcc82bf38f09a7b425c095d92bf",
"avax": "0x88de50b233052e4fb783d4f6db78cc34fea3e9fc",
"bsc": "0x89b8aa89fdd0507a99d334cbe3c808fafc7d850e",
"fantom": "0xd0c22a5435f4e8e5770c1fafb5374015fc12f7cd",
"zksync": "0x4bBa932E9792A2b917D47830C93a9BC79320E4f7",
"linea": "0x2d8879046f1559E53eb052E949e9544bCB72f414",
"scroll": "0xbFe03C9E20a9Fc0b37de01A172F207004935E0b1",
"sonic": "0xac041df48df9791b0654f1dbbf2cc8450c5f2e9d",
"unichain": "0x6409722F3a1C4486A3b1FE566cBDd5e9D946A1f3",
}
def odos_quote(chain, chain_id, from_tok, to_tok, amount_wei, wallet, slippage):
if chain not in ODOS_CHAINS:
return None
from_addr = from_tok["address"]
to_addr = to_tok["address"]
quote = requests.post(
"https://api.odos.xyz/sor/quote/v2",
json={
"chainId": chain_id,
"inputTokens": [{"tokenAddress": from_addr, "amount": amount_wei}],
"outputTokens": [{"tokenAddress": to_addr, "proportion": 1}],
"userAddr": wallet or ZERO_ADDR,
"slippageLimitPercent": float(slippage) if slippage else 1.0,
"referralCode": 2101375859,
"disableRFQs": True, "compact": True,
},
headers={"Content-Type": "application/json"},
timeout=15,
).json()
if "pathId" not in quote:
return None
result = {
"aggregator": "Odos",
"amountOut": str(quote.get("outAmounts", ["0"])[0]),
"amountIn": amount_wei,
"gas": str(quote.get("gasEstimate", 0)),
"tokenApprovalAddress": ODOS_ROUTERS.get(chain),
}
if wallet and wallet != ZERO_ADDR:
swap_data = requests.post(
"https://api.odos.xyz/sor/assemble",
json={"userAddr": wallet, "pathId": quote["pathId"]},
headers={"Content-Type": "application/json"},
timeout=15,
).json()
if "transaction" in swap_data:
tx = swap_data["transaction"]
if tx["to"].lower() == ODOS_ROUTERS.get(chain, "").lower():
result["tx"] = {
"to": tx["to"], "data": tx["data"],
"value": str(tx.get("value", "0")),
"gas": str(tx.get("gas") or swap_data.get("gasEstimate", 0)),
"from": tx["from"],
}
if swap_data.get("outputTokens"):
result["amountOut"] = str(swap_data["outputTokens"][0]["amount"])
else:
result["error"] = f"Router mismatch: {tx['to']} != {ODOS_ROUTERS.get(chain)}"
return result
# ── KyberSwap ─────────────────────────────────────────────────────────────────
KYBER_CHAIN_MAP = {
"ethereum": "ethereum", "bsc": "bsc", "polygon": "polygon",
"arbitrum": "arbitrum", "optimism": "optimism", "avax": "avalanche",
"fantom": "fantom", "base": "base", "linea": "linea",
"scroll": "scroll", "zksync": "zksync", "sonic": "sonic",
}
def kyberswap_quote(chain, chain_id, from_tok, to_tok, amount_wei, wallet, slippage):
kyber_chain = KYBER_CHAIN_MAP.get(chain)
if not kyber_chain:
return None
from_addr = NATIVE_PLACEHOLDER if from_tok["address"] == ZERO_ADDR else from_tok["address"]
to_addr = NATIVE_PLACEHOLDER if to_tok["address"] == ZERO_ADDR else to_tok["address"]
url = (
f"https://aggregator-api.kyberswap.com/{kyber_chain}/api/v1/routes?"
f"tokenIn={from_addr}&tokenOut={to_addr}&amountIn={amount_wei}"
f"&saveGas=false&gasInclude=true"
)
data = requests.get(url, timeout=15).json()
if data.get("code") != 0 or not data.get("data", {}).get("routeSummary"):
return None
summary = data["data"]["routeSummary"]
result = {
"aggregator": "KyberSwap",
"amountOut": summary.get("amountOut", "0"),
"amountIn": summary.get("amountIn", amount_wei),
"gas": summary.get("gasUsd", "0"),
"tokenApprovalAddress": summary.get("routerAddress"),
}
if wallet and wallet != ZERO_ADDR:
slippage_bps = int(float(slippage) * 100) if slippage else 100
build_data = requests.post(
f"https://aggregator-api.kyberswap.com/{kyber_chain}/api/v1/route/build",
json={
"routeSummary": summary,
"sender": wallet, "recipient": wallet,
"slippageTolerance": slippage_bps,
},
headers={"Content-Type": "application/json"},
timeout=15,
).json()
if build_data.get("code") == 0 and build_data.get("data"):
tx_data = build_data["data"]
result["tx"] = {
"to": tx_data["routerAddress"], "data": tx_data["data"],
"value": tx_data.get("value", "0"),
"gas": tx_data.get("gas", "0"),
"from": wallet,
}
return result
# ============================================================
# 0x / Matcha (requires OX_API_KEY env var - free at 0x.org/pricing)
# ============================================================
ZEROX_CHAIN_IDS = {
"ethereum": "1", "bsc": "56", "polygon": "137", "optimism": "10",
"arbitrum": "42161", "avalanche": "43114", "base": "8453",
"linea": "59144", "scroll": "534352", "unichain": "130"
}
ZEROX_PERMIT2_ADDRESS = "0x000000000022d473030f116ddee9f6b43ac78ba3"
def zerox_quote(chain, chain_id, from_tok, to_tok, amount_wei, wallet, slippage):
"""Get quote from 0x/Matcha v2 (permit2) API."""
api_key = os.environ.get("OX_API_KEY", "")
if not api_key:
return None # silently skip if no key configured
zx_chain_id = ZEROX_CHAIN_IDS.get(chain)
if not zx_chain_id:
return None
native = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
from_addr = from_tok["address"] if isinstance(from_tok, dict) else from_tok
to_addr = to_tok["address"] if isinstance(to_tok, dict) else to_tok
token_from = native if from_addr == ZERO_ADDR else from_addr
token_to = native if to_addr == ZERO_ADDR else to_addr
# 0x requires taker > 0x...ffff; use a real address as fallback for quotes
taker = wallet or "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
slippage_bps = int(float(slippage) * 100) if slippage else 100
try:
r = requests.get(
"https://api.0x.org/swap/permit2/quote",
params={
"chainId": zx_chain_id,
"buyToken": token_to,
"sellToken": token_from,
"sellAmount": amount_wei,
"slippageBps": str(slippage_bps),
"taker": taker,
},
headers={
"0x-api-key": api_key,
"0x-version": "v2"
},
timeout=15,
)
if r.status_code != 200:
return None
data = r.json()
buy_amount = data.get("buyAmount") or data.get("minBuyAmount", "0")
tx = data.get("transaction")
return {
"aggregator": "Matcha/0x",
"amountOut": buy_amount,
"gas": data.get("gas", tx.get("gas", "0") if tx else "0"),
"tx": tx,
"tokenApprovalAddress": ZEROX_PERMIT2_ADDRESS,
}
except Exception:
return None
# ── CowSwap ───────────────────────────────────────────────────────────────────
# CowSwap batch auction protocol — MEV-protected by design (solvers compete
# off-chain, no front-running possible). Uses CoW Protocol API v1.
COWSWAP_CHAINS = {"ethereum", "arbitrum", "gnosis", "base"}
COWSWAP_CHAIN_PREFIX = {
"ethereum": "mainnet", "arbitrum": "arbitrum_one", "gnosis": "xdai", "base": "base",
}
COWSWAP_NATIVE_TOKEN = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
# CowSwap wrapped native addresses per chain (required — CowSwap can't quote raw native)
COWSWAP_WRAPPED_NATIVE = {
"ethereum": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", # WETH
"arbitrum": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # WETH
"gnosis": "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d", # WXDAI
"base": "0x4200000000000000000000000000000000000006", # WETH
}
def cowswap_quote(chain, chain_id, from_tok, to_tok, amount_wei, wallet, slippage):
"""Get a quote from CowSwap (CoW Protocol). MEV-protected batch auctions."""
if chain not in COWSWAP_CHAINS:
return None
prefix = COWSWAP_CHAIN_PREFIX[chain]
sell_token = from_tok["address"]
buy_token = to_tok["address"]
# CowSwap doesn't support native ETH directly — must use wrapped version
if sell_token == ZERO_ADDR or sell_token.lower() == COWSWAP_NATIVE_TOKEN.lower():
sell_token = COWSWAP_WRAPPED_NATIVE.get(chain)
if not sell_token:
return None
if buy_token == ZERO_ADDR or buy_token.lower() == COWSWAP_NATIVE_TOKEN.lower():
buy_token = COWSWAP_WRAPPED_NATIVE.get(chain)
if not buy_token:
return None
sender = wallet if wallet and wallet != ZERO_ADDR else "0x0000000000000000000000000000000000000001"
try:
resp = requests.post(
f"https://api.cow.fi/{prefix}/api/v1/quote",
json={
"sellToken": sell_token,
"buyToken": buy_token,
"sellAmountBeforeFee": str(amount_wei),
"from": sender,
"kind": "sell",
"receiver": sender,
"appData": "0x0000000000000000000000000000000000000000000000000000000000000000",
"appDataHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"partiallyFillable": False,
"sellTokenBalance": "erc20",
"buyTokenBalance": "erc20",
"signingScheme": "eip712",
},
headers={"Content-Type": "application/json"},
timeout=15,
)
if resp.status_code != 200:
return None
data = resp.json()
except Exception:
return None
quote = data.get("quote", {})
buy_amount = quote.get("buyAmount", "0")
fee_amount = quote.get("feeAmount", "0")
sell_amount = quote.get("sellAmount", "0")
result = {
"aggregator": "CowSwap",
"amountOut": buy_amount,
"amountIn": sell_amount,
"gas": "0", # CowSwap is gasless for the user (solvers pay gas)
"feeAmount": fee_amount, # Protocol fee taken from sell token
"tokenApprovalAddress": quote.get("receiver", f"0x40A50cf069e992AA4536211B23F286eF88752187"),
"isMEVSafe": True,
}
# CowSwap execution is order-based (EIP-712 signed intent, not raw tx).
# For execution, agent posts a signed order to the CowSwap API.
# The "tx" here carries the order payload for signing.
if wallet and wallet != ZERO_ADDR:
result["tx"] = {
"to": f"https://api.cow.fi/{prefix}/api/v1/orders",
"method": "POST",
"type": "cowswap_order",
"order": {
"sellToken": sell_token,
"buyToken": buy_token,
"sellAmount": sell_amount,
"buyAmount": buy_amount,
"feeAmount": fee_amount,
"kind": "sell",
"receiver": wallet,
"validTo": data.get("quote", {}).get("validTo", 0),
"appData": "0x0000000000000000000000000000000000000000000000000000000000000000",
"partiallyFillable": False,
},
"quoteId": data.get("id"),
}
return result
# ── 1inch ─────────────────────────────────────────────────────────────────────
# 1inch is NOT called from this script. It uses the platform's native
# oneinch_quote / oneinch_swap tools (proxied, no user API key needed).
# The agent merges the 1inch result into the comparison table.
# See SKILL.md for the workflow.
# ── Dispatch ──────────────────────────────────────────────────────────────────
AGGREGATORS = {
"paraswap": paraswap_quote,
"odos": odos_quote,
"kyberswap": kyberswap_quote,
"matcha/0x": zerox_quote,
"cowswap": cowswap_quote,
# "1inch" — handled via native oneinch_quote tool, not in this script
}
"""Chain and token constants for Meta DEX Aggregator."""
CHAINS = {
"ethereum": 1, "bsc": 56, "polygon": 137, "optimism": 10,
"arbitrum": 42161, "avax": 43114, "gnosis": 100, "fantom": 250,
"zksync": 324, "base": 8453, "linea": 59144, "scroll": 534352,
"sonic": 146, "unichain": 130,
}
ZERO_ADDR = "0x0000000000000000000000000000000000000000"
NATIVE_PLACEHOLDER = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
NATIVE_SYMBOLS = {
"ethereum": "ETH", "bsc": "BNB", "polygon": "POL", "optimism": "ETH",
"arbitrum": "ETH", "avax": "AVAX", "gnosis": "xDAI", "fantom": "FTM",
"zksync": "ETH", "base": "ETH", "linea": "ETH", "scroll": "ETH",
"sonic": "S", "unichain": "ETH",
}
DEFILLAMA_REFERRER = "0x08a3c2A819E3de7ACa384c798269B3Ce1CD0e437"
"""Cross-chain aggregator adapters for Meta DEX Aggregator.
Queries LI.FI (multi-bridge) and 1inch Fusion+ for cross-chain quotes.
Returns standardized results compatible with the same-chain pipeline.
"""
import os, json, requests
from chains import CHAINS
# ── Proxy Configuration ─────────────────────────────────────────────────────
# Platform uses transparent proxy (sc-proxy) for billing on whitelisted APIs.
# LI.FI is NOT whitelisted — use direct connection.
# Configure proxies from environment if available (used for other APIs if needed).
PROXIES = {} # LI.FI uses direct connection (no proxy)
_proxy_host = os.environ.get("PROXY_HOST", "")
_proxy_port = os.environ.get("PROXY_PORT", "")
if _proxy_host and _proxy_port:
# Handle IPv6 addresses by wrapping in brackets
if ":" in _proxy_host and not _proxy_host.startswith("["):
_proxy_host = f"[{_proxy_host}]"
PROXY_URL = f"http://{_proxy_host}:{_proxy_port}"
# Store for potential use by other APIs, but LI.FI calls use PROXIES = {}
_PLATFORM_PROXIES = {"http": PROXY_URL, "https": PROXY_URL}
else:
_PLATFORM_PROXIES = {}
# ── Chain ID mappings ────────────────────────────────────────────────────────
LIFI_CHAIN_IDS = {
"ethereum": 1, "arbitrum": 42161, "optimism": 10, "base": 8453,
"polygon": 137, "bsc": 56, "avax": 43114, "fantom": 250,
"gnosis": 100, "zksync": 324, "scroll": 534352, "linea": 59144,
"blast": 81457, "sonic": 146,
}
# 1inch Fusion+ supported chains (chain name -> 1inch chain identifier)
ONEINCH_XCHAIN = {
"ethereum", "arbitrum", "optimism", "base", "polygon", "bsc", "avax", "gnosis",
}
LIFI_NATIVE = "0x0000000000000000000000000000000000000000"
ONEINCH_NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
# ── LI.FI ────────────────────────────────────────────────────────────────────
def lifi_resolve_token(chain_id, symbol_or_addr):
"""Resolve token via LI.FI's own token endpoint."""
if symbol_or_addr.startswith("0x") and len(symbol_or_addr) == 42:
return symbol_or_addr
try:
r = requests.get(
f"https://li.quest/v1/token",
params={"chain": chain_id, "token": symbol_or_addr},
timeout=10,
proxies=PROXIES,
)
if r.status_code == 200:
data = r.json()
return data.get("address", symbol_or_addr)
except Exception:
pass
return symbol_or_addr
def lifi_quote(src_chain, dst_chain, from_tok, to_tok, amount_wei, wallet, slippage):
"""Get cross-chain quotes from LI.FI advanced routes API.
Returns list of route options (LI.FI often returns multiple bridge paths).
"""
src_chain_id = LIFI_CHAIN_IDS.get(src_chain)
dst_chain_id = LIFI_CHAIN_IDS.get(dst_chain)
if not src_chain_id or not dst_chain_id:
return []
from_addr = from_tok["address"]
to_addr = to_tok["address"]
# LI.FI uses 0x000...000 for native tokens
if from_addr.lower() == ONEINCH_NATIVE.lower():
from_addr = LIFI_NATIVE
if to_addr.lower() == ONEINCH_NATIVE.lower():
to_addr = LIFI_NATIVE
slip = float(slippage) / 100 if float(slippage) > 1 else float(slippage)
try:
r = requests.post(
"https://li.quest/v1/advanced/routes",
json={
"fromChainId": src_chain_id,
"toChainId": dst_chain_id,
"fromTokenAddress": from_addr,
"toTokenAddress": to_addr,
"fromAmount": str(amount_wei),
"fromAddress": wallet,
"options": {
"slippage": slip,
"order": "RECOMMENDED",
"maxPriceImpact": 0.5, # 50% max — cross-chain routes can have higher impact
},
},
headers={"Content-Type": "application/json"},
timeout=30,
proxies=PROXIES,
)
data = r.json()
except Exception as e:
return [{"aggregator": "LI.FI", "error": str(e)}]
routes = data.get("routes", [])
results = []
for route in routes[:5]: # cap at 5 routes
steps = route.get("steps", [])
bridge_names = [s.get("toolDetails", {}).get("name", "?") for s in steps]
route_label = " → ".join(bridge_names)
est_time = sum(s.get("estimate", {}).get("executionDuration", 0) for s in steps)
gas_usd = float(route.get("gasCostUSD", "0"))
# Collect all fee costs
total_fees_usd = gas_usd
fee_breakdown = []
for s in steps:
for fc in s.get("estimate", {}).get("feeCosts", []):
fee_usd = float(fc.get("amountUSD", "0"))
total_fees_usd += fee_usd
fee_breakdown.append({"name": fc.get("name", "fee"), "usd": fee_usd})
to_amount = route.get("toAmount", "0")
to_amount_min = route.get("toAmountMin", "0")
result = {
"aggregator": f"LI.FI ({route_label})",
"amountOut": to_amount,
"amountOutMin": to_amount_min,
"amountIn": str(amount_wei),
"gasUsd": gas_usd,
"totalFeesUsd": total_fees_usd,
"feeBreakdown": fee_breakdown,
"estimatedTimeSeconds": est_time,
"tags": route.get("tags", []),
"crosschain": True,
"execution": "lifi",
}
# Include transaction data for execution
if steps and steps[0].get("transactionRequest"):
tx_req = steps[0]["transactionRequest"]
result["tx"] = {
"to": tx_req.get("to"),
"data": tx_req.get("data"),
"value": tx_req.get("value", "0"),
"gas": tx_req.get("gasLimit", "0"),
"from": tx_req.get("from", wallet),
"chainId": src_chain_id,
}
# Store full route for step-by-step execution if needed
result["_lifi_route"] = route
results.append(result)
return results
# ── 1inch Fusion+ ────────────────────────────────────────────────────────────
def oneinch_fusion_quote(src_chain, dst_chain, from_tok, to_tok, amount_wei, wallet, slippage):
"""Get cross-chain quote from 1inch Fusion+.
NOTE: This returns quote data. Actual execution uses the oneinch_cross_chain_swap
tool (intent-based, handled by the agent, not raw tx).
"""
if src_chain not in ONEINCH_XCHAIN or dst_chain not in ONEINCH_XCHAIN:
return []
from_addr = from_tok["address"]
to_addr = to_tok["address"]
# 1inch native = 0xEeee...
if from_addr == "0x0000000000000000000000000000000000000000":
from_addr = ONEINCH_NATIVE
if to_addr == "0x0000000000000000000000000000000000000000":
to_addr = ONEINCH_NATIVE
# We can't call the proxied 1inch API directly from scripts.
# Instead, return a marker that the agent should use oneinch_cross_chain_quote tool.
return [{
"aggregator": "1inch Fusion+",
"srcChain": src_chain,
"dstChain": dst_chain,
"srcToken": from_addr,
"dstToken": to_addr,
"amountIn": str(amount_wei),
"crosschain": True,
"execution": "oneinch_fusion",
"_needs_tool_call": True,
"note": "Quote requires agent tool call (oneinch_cross_chain_quote)",
}]
# ── Dispatch ─────────────────────────────────────────────────────────────────
def get_crosschain_quotes(src_chain, dst_chain, from_tok, to_tok, amount_wei, wallet, slippage="1"):
"""Get quotes from all cross-chain aggregators.
Returns list of quote results from LI.FI and 1inch Fusion+.
"""
results = []
# LI.FI — returns ready-to-execute tx data
lifi_results = lifi_quote(src_chain, dst_chain, from_tok, to_tok, amount_wei, wallet, slippage)
results.extend(lifi_results)
# 1inch Fusion+ — returns marker for agent tool call
oneinch_results = oneinch_fusion_quote(src_chain, dst_chain, from_tok, to_tok, amount_wei, wallet, slippage)
results.extend(oneinch_results)
return results
#!/usr/bin/env python3
"""
Meta DEX Aggregator — Multi-source quote comparison CLI.
Queries ParaSwap, Odos, KyberSwap, Matcha/0x, and 1inch for the best swap route.
Usage:
python3 skills/meta-dex-aggregator/scripts/meta_dex.py search --chain ethereum --query USDC
python3 skills/meta-dex-aggregator/scripts/meta_dex.py quote --chain ethereum --from ETH --to USDC --amount 1.0
python3 skills/meta-dex-aggregator/scripts/meta_dex.py swap --chain base --from ETH --to USDC --amount 0.5 --aggregator odos --wallet 0x...
python3 skills/meta-dex-aggregator/scripts/meta_dex.py xquote --src-chain arbitrum --dst-chain polygon --from ETH --to USDC --amount 0.5 --wallet 0x...
python3 skills/meta-dex-aggregator/scripts/meta_dex.py execute --chain base --from ETH --to USDC --amount 0.5 --aggregator kyberswap --wallet 0x... --verify
python3 skills/meta-dex-aggregator/scripts/meta_dex.py monitor --chain arbitrum --from ETH --to USDC --amount 1.0 --interval 60 --target-net-out 2050
"""
import argparse, json, sys, os, time, hashlib
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed
from chains import CHAINS
from tokens import search_tokens, resolve_token, to_wei, from_wei
from aggregators import AGGREGATORS, ZERO_ADDR
from safety import full_safety_check
from crosschain import get_crosschain_quotes, LIFI_CHAIN_IDS
from quote_logger import log_quote
def get_all_quotes(chain, from_tok, to_tok, amount_wei, wallet=None, slippage=None):
chain_id = CHAINS[chain]
results = []
with ThreadPoolExecutor(max_workers=5) as pool:
futures = {
pool.submit(func, chain, chain_id, from_tok, to_tok, amount_wei, wallet, slippage): name
for name, func in AGGREGATORS.items()
}
for f in as_completed(futures):
try:
r = f.result()
if r:
results.append(r)
except Exception:
pass
results.sort(key=lambda r: int(r.get("amountOut", "0")), reverse=True)
return results
def cmd_search(args):
results = search_tokens(args.chain, args.query, limit=args.limit)
out = [{"symbol": t["symbol"], "name": t["name"], "address": t["address"],
"decimals": t["decimals"], "volume24h": t.get("volume24h")} for t in results]
print(json.dumps(out, indent=2))
def cmd_quote(args):
from_tok = resolve_token(args.chain, getattr(args, "from"))
to_tok = resolve_token(args.chain, args.to)
amount_wei = to_wei(args.amount, from_tok["decimals"])
quotes = get_all_quotes(args.chain, from_tok, to_tok, amount_wei,
getattr(args, "wallet", None), getattr(args, "slippage", "1.0"))
# Build quote list for safety check
quote_data = []
for q in quotes:
human_out = from_wei(q["amountOut"], to_tok["decimals"])
quote_data.append({
"name": q["aggregator"], "outputAmount": float(human_out),
"estimatedGas": int(float(q.get("gas", "0") or 0)),
"amountOut": q["amountOut"],
"tokenApprovalAddress": q.get("tokenApprovalAddress"),
})
slippage = getattr(args, "slippage", "0.5")
safety = full_safety_check(
quote_data, args.chain, from_tok["address"], to_tok["address"],
from_tok["symbol"], to_tok["symbol"], float(args.amount), slippage
)
output = {"chain": args.chain, "fromToken": from_tok, "toToken": to_tok,
"amountIn": args.amount, "amountInWei": amount_wei,
"safety": {
"recommendation": safety["recommendation"],
"priceImpact": safety["price_impact"],
"slippageWarnings": safety["slippage_warnings"],
"marketPrices": safety["prices"],
},
"quotes": []}
best_aggregator = None
best_net_out = 0
for r in safety["ranked_quotes"]:
entry = {"aggregator": r["name"],
"amountOutHuman": r["outputAmount"],
"amountUsd": r.get("amountUsd"),
"gasUsd": r.get("gasUsd"),
"netOut": r.get("netOut"),
"vsbestPct": r.get("vsbestPct"),
"isMEVSafe": r.get("isMEVSafe", False),
"isOutlier": r.get("isOutlier", False)}
output["quotes"].append(entry)
# Track best for logging
if r.get("netOut") and float(r["netOut"]) > best_net_out:
best_net_out = float(r["netOut"])
best_aggregator = r["name"]
# Log this quote for historical analysis
if best_aggregator and best_net_out > 0:
try:
log_id = log_quote(
chain=args.chain,
from_token=from_tok,
to_token=to_tok,
amount_in=args.amount,
quotes=output["quotes"],
best_aggregator=best_aggregator,
net_out_usd=best_net_out
)
output["_logId"] = log_id
except Exception:
pass # Logging is optional, don't fail the quote
print(json.dumps(output, indent=2))
def cmd_swap(args):
if not args.wallet:
print(json.dumps({"error": "Wallet address required. Use --wallet 0x..."}))
sys.exit(1)
from_tok = resolve_token(args.chain, getattr(args, "from"))
to_tok = resolve_token(args.chain, args.to)
amount_wei = to_wei(args.amount, from_tok["decimals"])
agg = args.aggregator.lower()
if agg not in AGGREGATORS:
print(json.dumps({"error": f"Unknown aggregator: {agg}. Available: {list(AGGREGATORS.keys())}"}))
sys.exit(1)
chain_id = CHAINS[args.chain]
result = AGGREGATORS[agg](args.chain, chain_id, from_tok, to_tok, amount_wei, args.wallet, args.slippage)
if not result:
print(json.dumps({"error": f"{agg} returned no quote for this pair on {args.chain}"}))
sys.exit(1)
if "tx" not in result:
print(json.dumps({"error": f"{agg} returned a quote but no swap tx data."}))
sys.exit(1)
print(json.dumps({
"chain": args.chain, "chainId": chain_id, "aggregator": result["aggregator"],
"fromToken": from_tok, "toToken": to_tok,
"amountIn": args.amount, "amountInWei": amount_wei,
"amountOut": result["amountOut"],
"amountOutHuman": from_wei(result["amountOut"], to_tok["decimals"]),
"tokenApprovalAddress": result.get("tokenApprovalAddress"),
"needsApproval": from_tok["address"] != ZERO_ADDR,
"tx": result["tx"],
}, indent=2))
def cmd_xquote(args):
"""Cross-chain quote: compare routes across LI.FI and 1inch Fusion+."""
src_chain = args.src_chain
dst_chain = args.dst_chain
from_tok = resolve_token(src_chain, getattr(args, "from"))
to_tok = resolve_token(dst_chain, args.to)
amount_wei = to_wei(args.amount, from_tok["decimals"])
wallet = getattr(args, "wallet", None) or ZERO_ADDR
quotes = get_crosschain_quotes(
src_chain, dst_chain, from_tok, to_tok, amount_wei, wallet,
getattr(args, "slippage", "3")
)
output = {
"type": "crosschain",
"srcChain": src_chain,
"dstChain": dst_chain,
"fromToken": from_tok,
"toToken": to_tok,
"amountIn": args.amount,
"amountInWei": str(amount_wei),
"quotes": [],
}
for q in quotes:
if q.get("_needs_tool_call"):
# 1inch Fusion+ marker — agent needs to call oneinch_cross_chain_quote
output["quotes"].append({
"aggregator": q["aggregator"],
"execution": q["execution"],
"needsToolCall": True,
"srcToken": q.get("srcToken"),
"dstToken": q.get("dstToken"),
"note": q.get("note"),
})
else:
human_out = from_wei(q.get("amountOut", "0"), to_tok["decimals"])
human_min = from_wei(q.get("amountOutMin", q.get("amountOut", "0")), to_tok["decimals"])
entry = {
"aggregator": q["aggregator"],
"amountOutHuman": human_out,
"amountOutMinHuman": human_min,
"gasUsd": q.get("gasUsd", 0),
"totalFeesUsd": q.get("totalFeesUsd", 0),
"feeBreakdown": q.get("feeBreakdown", []),
"estimatedTimeSeconds": q.get("estimatedTimeSeconds", 0),
"tags": q.get("tags", []),
"execution": q.get("execution"),
"hasTxData": "tx" in q,
}
if q.get("error"):
entry["error"] = q["error"]
output["quotes"].append(entry)
print(json.dumps(output, indent=2))
def cmd_execute(args):
"""
Execute a swap with built-in verification.
Workflow:
1. Get quote from specified aggregator
2. Print pre-swap balances (requires wallet_balance tool call by agent)
3. Return tx data for execution
4. Agent executes via wallet_transfer
5. Agent calls this again with --verify to check post-swap balances
v3.1.0: Added --market-order flag for instant execution via 1inch
v3.1.0: Added --auto-verify to auto-fetch balances (no manual args needed)
"""
if not args.wallet:
print(json.dumps({"error": "Wallet address required. Use --wallet 0x..."}))
sys.exit(1)
chain = args.chain
from_tok = resolve_token(chain, getattr(args, "from"))
to_tok = resolve_token(chain, args.to)
amount_wei = to_wei(args.amount, from_tok["decimals"])
# Check if using market order mode
use_market_order = getattr(args, "market_order", False)
# Market order mode: use 1inch directly for instant execution (no aggregator needed)
if use_market_order:
# Get 1inch quote via native tool
from aggregators import ZERO_ADDR
from_tok_addr = from_tok["address"] if from_tok["address"] != ZERO_ADDR else "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
to_tok_addr = to_tok["address"]
# Return marker for agent to call oneinch_swap
result = {
"step": "market_order_ready",
"mode": "market_order",
"chain": chain,
"fromToken": from_tok,
"toToken": to_tok,
"amountIn": args.amount,
"amountInWei": amount_wei,
"timestamp": datetime.utcnow().isoformat() + "Z",
"instruction": "Execute via oneinch_swap(chain='{chain}', src='{from_addr}', dst='{to_addr}', amount='{amount_wei}', slippage={slippage})",
"fromAddr": from_tok_addr,
"toAddr": to_tok_addr,
"slippage": getattr(args, "slippage", "2.0"), # Default 2% for market orders
}
print(json.dumps(result, indent=2))
return
# Normal limit order mode requires aggregator
if not args.aggregator:
print(json.dumps({"error": "Aggregator required for limit orders. Use --aggregator kyberswap, or use --market-order for 1inch market order"}))
sys.exit(1)
agg = args.aggregator.lower()
# Normal limit order mode
chain_id = CHAINS[chain]
if agg not in AGGREGATORS:
print(json.dumps({"error": f"Unknown aggregator: {agg}. Available: {list(AGGREGATORS.keys())}"}))
sys.exit(1)
quote = AGGREGATORS[agg](chain, chain_id, from_tok, to_tok, amount_wei, args.wallet, args.slippage)
if not quote:
print(json.dumps({"error": f"{agg} returned no quote for this pair on {chain}"}))
sys.exit(1)
if "tx" not in quote and agg != "cowswap":
print(json.dumps({"error": f"{agg} returned a quote but no swap tx data."}))
sys.exit(1)
# Build execution package
result = {
"step": "pre_execution",
"chain": chain,
"chainId": chain_id,
"aggregator": quote["aggregator"],
"fromToken": from_tok,
"toToken": to_tok,
"amountIn": args.amount,
"amountInWei": amount_wei,
"expectedOut": quote["amountOut"],
"expectedOutHuman": from_wei(quote["amountOut"], to_tok["decimals"]),
"slippage": args.slippage,
"timestamp": datetime.utcnow().isoformat() + "Z",
}
# CowSwap order handling
if agg == "cowswap":
result["orderType"] = "cowswap_order"
result["orderUid"] = quote.get("orderUid")
result["eip712Data"] = quote.get("eip712Data")
result["note"] = "CowSwap order - sign EIP-712 and poll for fulfillment"
result["pollEndpoint"] = f"https://api.cow.fi/{chain}/api/v1/orders/{quote.get('orderUid')}"
else:
result["tx"] = quote["tx"]
result["needsApproval"] = from_tok["address"] != ZERO_ADDR
result["tokenApprovalAddress"] = quote.get("tokenApprovalAddress")
# Verification instructions
result["verification"] = {
"instruction": "After execution, call: python3 meta_dex.py execute --chain {chain} --from {from_tok} --to {to_tok} --amount {amount} --aggregator {agg} --wallet {wallet} --verify --expected-out {expected_out}",
"expectedOutWei": quote["amountOut"],
"maxDeviationPct": args.max_deviation if hasattr(args, "max_deviation") else 2.0,
}
print(json.dumps(result, indent=2))
def cmd_monitor(args):
"""
Monitor for target net output. Polls quotes at interval until target is met.
Use case: "Alert me when ETH→USDC on Arbitrum nets >$2050 after gas"
"""
chain = args.chain
from_tok = resolve_token(chain, getattr(args, "from"))
to_tok = resolve_token(chain, args.to)
amount_wei = to_wei(args.amount, from_tok["decimals"])
interval = args.interval
target_net_out = float(args.target_net_out)
max_runs = args.max_runs if hasattr(args, "max_runs") and args.max_runs else 0
print(json.dumps({
"mode": "monitor",
"chain": chain,
"fromToken": from_tok["symbol"],
"toToken": to_tok["symbol"],
"amount": args.amount,
"targetNetOutUsd": target_net_out,
"intervalSeconds": interval,
"maxRuns": max_runs if max_runs else "unlimited",
"startedAt": datetime.utcnow().isoformat() + "Z",
}, indent=2), file=sys.stderr)
run_count = 0
while True:
run_count += 1
if max_runs and run_count > max_runs:
print(json.dumps({"status": "max_runs_reached", "runs": run_count}))
break
# Get all quotes
quotes = get_all_quotes(chain, from_tok, to_tok, amount_wei, getattr(args, "wallet", None), "1.0")
# Find best net out
best_quote = None
best_net_out = 0
for q in quotes:
net_out = float(q.get("netOut", 0))
if net_out > best_net_out:
best_net_out = net_out
best_quote = q
# Check if target met
if best_net_out >= target_net_out:
print(json.dumps({
"status": "TARGET_MET",
"run": run_count,
"timestamp": datetime.utcnow().isoformat() + "Z",
"bestAggregator": best_quote["aggregator"],
"netOutUsd": best_net_out,
"targetNetOutUsd": target_net_out,
"excessUsd": best_net_out - target_net_out,
"quote": best_quote,
}))
break
# Log progress
print(json.dumps({
"status": "waiting",
"run": run_count,
"timestamp": datetime.utcnow().isoformat() + "Z",
"bestNetOutUsd": best_net_out,
"targetNetOutUsd": target_net_out,
"gapUsd": target_net_out - best_net_out,
"bestAggregator": best_quote["aggregator"] if best_quote else None,
}), file=sys.stderr)
time.sleep(interval)
def cowswap_poll_order(chain, order_uid, timeout_sec=180, poll_interval=5):
"""
Poll CowSwap order until fulfilled or timeout.
Returns: {"status": "fulfilled"|"expired"|"cancelled"|"timeout", "orderData": {...}}
"""
import requests
endpoint = f"https://api.cow.fi/{chain}/api/v1/orders/{order_uid}"
start_time = time.time()
while time.time() - start_time < timeout_sec:
try:
r = requests.get(endpoint, timeout=10)
if r.status_code == 200:
data = r.json()
status = data.get("status")
if status == "fulfilled":
return {"status": "fulfilled", "orderData": data}
elif status in ["expired", "cancelled"]:
return {"status": status, "orderData": data}
except Exception as e:
pass
time.sleep(poll_interval)
return {"status": "timeout", "orderData": None, "error": f"Did not fulfill within {timeout_sec}s"}
def get_fallback_aggregators(chain, from_tok, to_tok, amount_wei, wallet, slippage, exclude=None):
"""
Get ranked list of fallback aggregators for retry logic.
Returns: List of (aggregator_name, quote) tuples sorted by netOut
"""
exclude = exclude or []
quotes = get_all_quotes(chain, from_tok, to_tok, amount_wei, wallet, slippage)
# Filter out excluded and sort by netOut
valid = []
for q in quotes:
if q["aggregator"].lower() not in exclude and not q.get("isOutlier", False):
valid.append(q)
valid.sort(key=lambda x: float(x.get("netOut", 0)), reverse=True)
return valid
def cmd_stats(args):
"""Show historical winner statistics for a trading pair."""
from quote_logger import get_winner_stats
result = get_winner_stats(args.chain, getattr(args, "from"), args.to, getattr(args, "days", 7))
print(json.dumps(result, indent=2))
def cmd_trend(args):
"""Show price trend (net output over time) for a trading pair."""
from quote_logger import get_price_trend
result = get_price_trend(
args.chain, getattr(args, "from"), args.to,
getattr(args, "days", 7), getattr(args, "bucket_hours", 1)
)
print(json.dumps(result, indent=2))
def cmd_slippage(args):
"""Show slippage analysis for a trading pair."""
from quote_logger import get_slippage_analysis
result = get_slippage_analysis(args.chain, getattr(args, "from"), args.to, getattr(args, "days", 7))
print(json.dumps(result, indent=2))
def cmd_export(args):
"""Export historical quotes to CSV."""
from quote_logger import export_to_csv
output_file = getattr(args, "output", None)
result = export_to_csv(args.chain, getattr(args, "from"), args.to, getattr(args, "days", 30), output_file)
if output_file:
print(json.dumps({"status": "exported", "file": output_file, "message": result}))
else:
print(result)
def verify_swap(args):
"""
Verify a swap was executed correctly by comparing expected vs actual.
v3.1.0: Added --auto-verify mode that auto-fetches balances from wallet
v3.1.1: Fixed RPC endpoints to use reliable public providers (Ankr, Cloudflare)
"""
chain = args.chain
from_tok = resolve_token(chain, getattr(args, "from"))
to_tok = resolve_token(chain, args.to)
from_decimals = from_tok["decimals"]
to_decimals = to_tok["decimals"]
# Auto-verify mode: fetch balances from wallet
if getattr(args, "auto_verify", False):
import requests
wallet = args.wallet
from aggregators import CHAINS
chain_id = CHAINS[chain]
# Fetch current balances via RPC
def get_token_balance(token_addr, decimals):
if token_addr == "0x0000000000000000000000000000000000000000":
# Native token - use reliable public RPCs
rpc_urls = {
1: "https://cloudflare-eth.com",
42161: "https://rpc.ankr.com/arbitrum",
8453: "https://rpc.ankr.com/base",
10: "https://rpc.ankr.com/optimism",
137: "https://rpc.ankr.com/polygon",
56: "https://rpc.ankr.com/bsc",
43114: "https://rpc.ankr.com/avalanche",
250: "https://rpc.ankr.com/fantom",
}
rpc = rpc_urls.get(chain_id)
if not rpc:
print(json.dumps({"error": f"No RPC URL for chain {chain}"}))
sys.exit(1)
payload = {"jsonrpc": "2.0", "method": "eth_getBalance", "params": [wallet, "latest"], "id": 1}
r = requests.post(rpc, json=payload, timeout=10)
if r.status_code == 200:
result = r.json()
return int(result.get("result", "0x0"), 16)
return 0
else:
# ERC20 - use reliable public RPCs
rpc_urls = {
1: "https://cloudflare-eth.com",
42161: "https://rpc.ankr.com/arbitrum",
8453: "https://rpc.ankr.com/base",
10: "https://rpc.ankr.com/optimism",
137: "https://rpc.ankr.com/polygon",
56: "https://rpc.ankr.com/bsc",
43114: "https://rpc.ankr.com/avalanche",
250: "https://rpc.ankr.com/fantom",
}
rpc = rpc_urls.get(chain_id)
if not rpc:
return 0
# balanceOf method
data = f"0x70a08231000000000000000000000000{wallet[2:]}"
payload = {"jsonrpc": "2.0", "method": "eth_call", "params": [{"to": token_addr, "data": data}, "latest"], "id": 1}
r = requests.post(rpc, json=payload, timeout=10)
if r.status_code == 200:
result = r.json()
return int(result.get("result", "0x0"), 16)
return 0
# Try to fetch balances with retry logic
max_retries = 3
for attempt in range(max_retries):
try:
post_from_wei = get_token_balance(from_tok["address"], from_decimals)
post_to_wei = get_token_balance(to_tok["address"], to_decimals)
break # Success
except Exception as e:
if attempt == max_retries - 1:
print(json.dumps({"error": f"Auto-verify failed after {max_retries} attempts: {str(e)}"}))
sys.exit(1)
time.sleep(1) # Wait before retry
# For auto-verify, we need pre-balances from args or estimate from expected
if args.pre_from_balance:
pre_from_wei = to_wei(float(args.pre_from_balance), from_decimals)
else:
pre_from_wei = post_from_wei + to_wei(float(args.amount), from_decimals)
if args.pre_to_balance:
pre_to_wei = to_wei(float(args.pre_to_balance), to_decimals)
else:
pre_to_wei = post_to_wei - (args.expected_out if args.expected_out else 0)
else:
# Manual mode (v3.0.0)
if not args.pre_from_balance or not args.pre_to_balance:
print(json.dumps({"error": "Pre-swap balances required. Use --pre-from-balance and --pre-to-balance, or use --auto-verify"}))
sys.exit(1)
if not args.post_from_balance or not args.post_to_balance:
print(json.dumps({"error": "Post-swap balances required. Use --post-from-balance and --post-to-balance, or use --auto-verify"}))
sys.exit(1)
pre_from_wei = to_wei(float(args.pre_from_balance), from_decimals)
post_from_wei = to_wei(float(args.post_from_balance), from_decimals)
pre_to_wei = to_wei(float(args.pre_to_balance), to_decimals)
post_to_wei = to_wei(float(args.post_to_balance), to_decimals)
# Calculate actual amounts
actual_spent_wei = pre_from_wei - post_from_wei
actual_received_wei = post_to_wei - pre_to_wei
actual_spent = from_wei(actual_spent_wei, from_decimals)
actual_received = from_wei(actual_received_wei, to_decimals)
expected_received_wei = args.expected_out if args.expected_out else "0"
expected_received = from_wei(expected_received_wei, to_decimals)
# Calculate deviation
if float(expected_received) > 0:
deviation_pct = ((float(actual_received) - float(expected_received)) / float(expected_received)) * 100
else:
deviation_pct = 0
max_deviation = args.max_deviation if hasattr(args, "max_deviation") else 2.0
passed = abs(deviation_pct) <= max_deviation
result = {
"verification": "PASSED" if passed else "FAILED",
"chain": args.chain,
"aggregator": args.aggregator,
"expectedSpent": args.amount,
"expectedReceived": expected_received,
"actualSpent": actual_spent,
"actualReceived": actual_received,
"deviationPct": round(deviation_pct, 3),
"maxDeviationPct": max_deviation,
"priceAchieved": float(actual_received) / float(actual_spent) if float(actual_spent) > 0 else 0,
"timestamp": datetime.utcnow().isoformat() + "Z",
}
if not passed:
result["warning"] = f"Deviation {deviation_pct:.2f}% exceeds threshold {max_deviation}% — check for slippage, MEV, or partial fill"
print(json.dumps(result, indent=2))
def main():
p = argparse.ArgumentParser(description="Meta DEX Aggregator")
sub = p.add_subparsers(dest="command", required=True)
s = sub.add_parser("search")
s.add_argument("--chain", required=True, choices=list(CHAINS.keys()))
s.add_argument("--query", required=True)
s.add_argument("--limit", type=int, default=20)
q = sub.add_parser("quote")
q.add_argument("--chain", required=True, choices=list(CHAINS.keys()))
q.add_argument("--from", required=True)
q.add_argument("--to", required=True)
q.add_argument("--amount", required=True)
q.add_argument("--wallet", default=None)
q.add_argument("--slippage", default="1.0")
w = sub.add_parser("swap")
w.add_argument("--chain", required=True, choices=list(CHAINS.keys()))
w.add_argument("--from", required=True)
w.add_argument("--to", required=True)
w.add_argument("--amount", required=True)
w.add_argument("--aggregator", required=True)
w.add_argument("--wallet", required=True)
w.add_argument("--slippage", default="1.0")
xq = sub.add_parser("xquote")
xq.add_argument("--src-chain", required=True)
xq.add_argument("--dst-chain", required=True)
xq.add_argument("--from", required=True)
xq.add_argument("--to", required=True)
xq.add_argument("--amount", required=True)
xq.add_argument("--wallet", required=True, help="Your wallet address (required for LI.FI quotes)")
xq.add_argument("--slippage", default="3")
# New: execute with verification
ex = sub.add_parser("execute")
ex.add_argument("--chain", required=True, choices=list(CHAINS.keys()))
ex.add_argument("--from", required=True)
ex.add_argument("--to", required=True)
ex.add_argument("--amount", required=True)
ex.add_argument("--aggregator", required=False, help="Aggregator for limit orders (not needed with --market-order)")
ex.add_argument("--wallet", required=True)
ex.add_argument("--slippage", default="2.0", help="Slippage tolerance %% (default 2.0%% for market orders)")
ex.add_argument("--max-deviation", type=float, default=2.0, help="Max deviation %% for verification")
ex.add_argument("--verify", action="store_true", help="Run verification mode (requires balance args)")
ex.add_argument("--auto-verify", action="store_true", help="Auto-fetch balances from wallet (no manual args needed)")
ex.add_argument("--market-order", action="store_true", help="Use 1inch market order for instant execution")
ex.add_argument("--pre-from-balance", type=float, help="Pre-swap from-token balance")
ex.add_argument("--pre-to-balance", type=float, help="Pre-swap to-token balance")
ex.add_argument("--post-from-balance", type=float, help="Post-swap from-token balance")
ex.add_argument("--post-to-balance", type=float, help="Post-swap to-token balance")
ex.add_argument("--expected-out", type=str, help="Expected output amount in wei (for verification)")
# New: monitor for target net output
m = sub.add_parser("monitor")
m.add_argument("--chain", required=True, choices=list(CHAINS.keys()))
m.add_argument("--from", required=True)
m.add_argument("--to", required=True)
m.add_argument("--amount", required=True)
m.add_argument("--wallet", default=None)
m.add_argument("--interval", type=int, default=60, help="Poll interval in seconds")
m.add_argument("--target-net-out", type=float, required=True, help="Target net output in USD")
m.add_argument("--max-runs", type=int, default=0, help="Max poll attempts (0=unlimited)")
# New: analytics commands
stats = sub.add_parser("stats")
stats.add_argument("--chain", required=True, choices=list(CHAINS.keys()))
stats.add_argument("--from", required=True)
stats.add_argument("--to", required=True)
stats.add_argument("--days", type=int, default=7, help="Analysis period in days")
trend = sub.add_parser("trend")
trend.add_argument("--chain", required=True, choices=list(CHAINS.keys()))
trend.add_argument("--from", required=True)
trend.add_argument("--to", required=True)
trend.add_argument("--days", type=int, default=7, help="Analysis period in days")
trend.add_argument("--bucket-hours", type=int, default=1, help="Data bucket size in hours")
slippage = sub.add_parser("slippage")
slippage.add_argument("--chain", required=True, choices=list(CHAINS.keys()))
slippage.add_argument("--from", required=True)
slippage.add_argument("--to", required=True)
slippage.add_argument("--days", type=int, default=7, help="Analysis period in days")
export = sub.add_parser("export")
export.add_argument("--chain", required=True, choices=list(CHAINS.keys()))
export.add_argument("--from", required=True)
export.add_argument("--to", required=True)
export.add_argument("--days", type=int, default=30, help="Export period in days")
export.add_argument("--output", default=None, help="Output CSV file path")
args = p.parse_args()
if args.command == "execute":
if args.verify:
verify_swap(args)
else:
cmd_execute(args)
elif args.command == "monitor":
cmd_monitor(args)
elif args.command == "stats":
cmd_stats(args)
elif args.command == "trend":
cmd_trend(args)
elif args.command == "slippage":
cmd_slippage(args)
elif args.command == "export":
cmd_export(args)
else:
{"search": cmd_search, "quote": cmd_quote, "swap": cmd_swap, "xquote": cmd_xquote}[args.command](args)
if __name__ == "__main__":
main()
"""
Historical quote logging for Meta DEX Aggregator.
Logs every quote to a JSONL file for later analysis:
- Which aggregator wins most often per chain/pair
- Price trends over time
- Gas cost patterns
- Slippage analysis
Usage:
from quote_logger import log_quote, get_winner_stats, get_price_trend
"""
import json, os
from datetime import datetime
from pathlib import Path
LOG_DIR = Path(os.path.dirname(os.path.abspath(__file__))).parent / "logs"
LOG_DIR.mkdir(exist_ok=True)
def _get_log_file(chain, from_symbol, to_symbol):
"""Get log file path for a specific trading pair."""
LOG_DIR.mkdir(exist_ok=True)
slug = f"{chain}_{from_symbol.upper()}_{to_symbol.upper()}.jsonl"
return LOG_DIR / slug
def log_quote(chain, from_token, to_token, amount_in, quotes, best_aggregator, net_out_usd):
"""
Log a quote snapshot to the historical log.
Args:
chain: Chain name (e.g., "arbitrum")
from_token: Token dict with symbol, address, decimals
to_token: Token dict with symbol, address, decimals
amount_in: Input amount (human-readable)
quotes: List of all aggregator quotes
best_aggregator: Name of winning aggregator
net_out_usd: Net output in USD (after gas)
Returns:
log_entry_id: Unique ID for this log entry
"""
log_file = _get_log_file(chain, from_token["symbol"], to_token["symbol"])
entry = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"chain": chain,
"fromToken": {
"symbol": from_token["symbol"],
"address": from_token["address"],
},
"toToken": {
"symbol": to_token["symbol"],
"address": to_token["address"],
},
"amountIn": amount_in,
"quotes": [
{
"aggregator": q["aggregator"],
"amountOut": q.get("amountOutHuman"),
"amountUsd": q.get("amountUsd"),
"gasUsd": q.get("gasUsd"),
"netOut": q.get("netOut"),
"vsBestPct": q.get("vsbestPct"),
"isMEVSafe": q.get("isMEVSafe", False),
}
for q in quotes
],
"winner": best_aggregator,
"netOutUsd": net_out_usd,
}
entry_id = f"{chain}-{from_token['symbol']}-{to_token['symbol']}-{int(datetime.utcnow().timestamp())}"
entry["id"] = entry_id
with open(log_file, "a") as f:
f.write(json.dumps(entry) + "\n")
return entry_id
def get_winner_stats(chain, from_symbol, to_symbol, days=7):
"""
Get statistics on which aggregator wins most often for a pair.
Args:
chain: Chain name
from_symbol: From token symbol
to_symbol: To token symbol
days: Number of days to analyze
Returns:
dict with winner counts, win rates, and average net out per aggregator
"""
import time
from datetime import timedelta
log_file = _get_log_file(chain, from_symbol, to_symbol)
if not log_file.exists():
return {"error": "No historical data for this pair"}
cutoff = datetime.utcnow() - timedelta(days=days)
winner_counts = {}
aggregator_net_outs = {}
total_entries = 0
with open(log_file, "r") as f:
for line in f:
entry = json.loads(line.strip())
ts = datetime.fromisoformat(entry["timestamp"].replace("Z", "+00:00")).replace(tzinfo=None)
if ts < cutoff:
continue
total_entries += 1
winner = entry["winner"]
winner_counts[winner] = winner_counts.get(winner, 0) + 1
# Track net outs per aggregator
for q in entry["quotes"]:
agg = q["aggregator"]
if agg not in aggregator_net_outs:
aggregator_net_outs[agg] = []
if q.get("netOut") is not None:
aggregator_net_outs[agg].append(float(q["netOut"]))
if total_entries == 0:
return {"error": f"No data in last {days} days"}
# Calculate win rates
win_rates = {agg: count / total_entries for agg, count in winner_counts.items()}
# Calculate average net out per aggregator
avg_net_outs = {}
for agg, net_outs in aggregator_net_outs.items():
if net_outs:
avg_net_outs[agg] = sum(net_outs) / len(net_outs)
return {
"chain": chain,
"pair": f"{from_symbol}/{to_symbol}",
"periodDays": days,
"totalQuotes": total_entries,
"winnerCounts": winner_counts,
"winRates": win_rates,
"mostFrequentWinner": max(winner_counts, key=winner_counts.get) if winner_counts else None,
"averageNetOutByAggregator": avg_net_outs,
"bestOnAverage": max(avg_net_outs, key=avg_net_outs.get) if avg_net_outs else None,
}
def get_price_trend(chain, from_symbol, to_symbol, days=7, bucket_hours=1):
"""
Get price trend (net output over time) for a pair.
Args:
chain: Chain name
from_symbol: From token symbol
to_symbol: To token symbol
days: Number of days to analyze
bucket_hours: Group data into N-hour buckets
Returns:
List of {timestamp, avgNetOut, minNetOut, maxNetOut, quoteCount} buckets
"""
from datetime import timedelta
from collections import defaultdict
log_file = _get_log_file(chain, from_symbol, to_symbol)
if not log_file.exists():
return {"error": "No historical data for this pair"}
cutoff = datetime.utcnow() - timedelta(days=days)
buckets = defaultdict(list)
with open(log_file, "r") as f:
for line in f:
entry = json.loads(line.strip())
ts = datetime.fromisoformat(entry["timestamp"].replace("Z", "+00:00")).replace(tzinfo=None)
if ts < cutoff:
continue
# Round to bucket
bucket_ts = ts.replace(minute=0, second=0, microsecond=0)
bucket_ts = bucket_ts.replace(hour=(bucket_ts.hour // bucket_hours) * bucket_hours)
if entry.get("netOutUsd") is not None:
buckets[bucket_ts].append(float(entry["netOutUsd"]))
# Build trend data
trend = []
for bucket_ts in sorted(buckets.keys()):
values = buckets[bucket_ts]
trend.append({
"timestamp": bucket_ts.isoformat() + "Z",
"avgNetOut": sum(values) / len(values),
"minNetOut": min(values),
"maxNetOut": max(values),
"quoteCount": len(values),
})
return {
"chain": chain,
"pair": f"{from_symbol}/{to_symbol}",
"periodDays": days,
"bucketHours": bucket_hours,
"data": trend,
}
def get_slippage_analysis(chain, from_symbol, to_symbol, days=7):
"""
Analyze slippage patterns (difference between expected and typical execution).
Returns:
Statistics on typical vs quoted spreads
"""
from datetime import timedelta
log_file = _get_log_file(chain, from_symbol, to_symbol)
if not log_file.exists():
return {"error": "No historical data for this pair"}
cutoff = datetime.utcnow() - timedelta(days=days)
spreads = [] # (best - second_best) / best
with open(log_file, "r") as f:
for line in f:
entry = json.loads(line.strip())
ts = datetime.fromisoformat(entry["timestamp"].replace("Z", "+00:00")).replace(tzinfo=None)
if ts < cutoff:
continue
# Get top 2 quotes by netOut
sorted_quotes = sorted(
[q for q in entry["quotes"] if q.get("netOut") is not None],
key=lambda q: q["netOut"],
reverse=True
)
if len(sorted_quotes) >= 2:
best = sorted_quotes[0]["netOut"]
second = sorted_quotes[1]["netOut"]
if best > 0:
spread = (best - second) / best * 100
spreads.append(spread)
if not spreads:
return {"error": "Insufficient data for analysis"}
return {
"chain": chain,
"pair": f"{from_symbol}/{to_symbol}",
"periodDays": days,
"samples": len(spreads),
"avgSpreadPct": sum(spreads) / len(spreads),
"minSpreadPct": min(spreads),
"maxSpreadPct": max(spreads),
"medianSpreadPct": sorted(spreads)[len(spreads) // 2],
"competitiveThreshold": "spread < 0.5% = highly competitive, >2% = one aggregator dominates",
}
def export_to_csv(chain, from_symbol, to_symbol, days=30, output_file=None):
"""
Export historical quotes to CSV for external analysis.
Returns:
CSV content as string, or writes to file if output_file specified
"""
from datetime import timedelta
import csv
from io import StringIO
log_file = _get_log_file(chain, from_symbol, to_symbol)
if not log_file.exists():
return "error,No historical data for this pair"
cutoff = datetime.utcnow() - timedelta(days=days)
rows = []
with open(log_file, "r") as f:
for line in f:
entry = json.loads(line.strip())
ts = datetime.fromisoformat(entry["timestamp"].replace("Z", "+00:00")).replace(tzinfo=None)
if ts < cutoff:
continue
# One row per aggregator quote
for q in entry["quotes"]:
rows.append({
"timestamp": entry["timestamp"],
"chain": chain,
"from_symbol": from_symbol,
"to_symbol": to_symbol,
"amount_in": entry["amountIn"],
"aggregator": q["aggregator"],
"amount_out": q.get("amountOut", ""),
"amount_usd": q.get("amountUsd", ""),
"gas_usd": q.get("gasUsd", ""),
"net_out": q.get("netOut", ""),
"vs_best_pct": q.get("vsBestPct", ""),
"is_mev_safe": q.get("isMEVSafe", False),
"winner": entry["winner"],
"net_out_usd": entry.get("netOutUsd", ""),
})
if not rows:
return "error,No data in specified period"
# Write CSV
output = StringIO()
fieldnames = ["timestamp", "chain", "from_symbol", "to_symbol", "amount_in",
"aggregator", "amount_out", "amount_usd", "gas_usd", "net_out",
"vs_best_pct", "is_mev_safe", "winner", "net_out_usd"]
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
csv_content = output.getvalue()
if output_file:
with open(output_file, "w") as f:
f.write(csv_content)
return f"Exported {len(rows)} rows to {output_file}"
return csv_content
"""
Meta DEX Aggregator Safety Layer — Price impact, slippage, gas-adjusted ranking, MEV flags.
Implements protective checks: price impact detection, gas-adjusted ranking, and outlier rejection.
Thresholds from constants.ts:
PRICE_IMPACT_WARNING = 3%, MEDIUM = 5%, HIGH = 10%
"""
import requests, json, sys
PRICE_IMPACT_WARNING = 3.0
PRICE_IMPACT_MEDIUM = 5.0
PRICE_IMPACT_HIGH = 10.0
SLIPPAGE_SANDWICH_RISK = 1.0
STABLECOIN_SLIPPAGE_MAX = 0.05
LOW_SLIPPAGE_THRESHOLD = 0.05
STABLECOINS = {
"USDT", "USDC", "BUSD", "DAI", "FRAX", "TUSD", "USDD", "USDP", "GUSD",
"LUSD", "sUSD", "FPI", "MIM", "DOLA", "USP", "USDX", "MAI", "EURS",
"EURT", "alUSD", "PAX", "USDS", "GHO", "crvUSD", "pyUSD", "USDe"
}
MEV_SAFE_AGGREGATORS = {"CowSwap", "0x Gasless", "Hashflow"}
DEFILLAMA_CHAIN_MAP = {
"ethereum": "ethereum", "bsc": "bsc", "polygon": "polygon",
"optimism": "optimism", "arbitrum": "arbitrum", "avax": "avax",
"avalanche": "avax", "gnosis": "xdai", "fantom": "fantom",
"zksync": "era", "base": "base", "linea": "linea",
"scroll": "scroll", "sonic": "sonic", "unichain": "unichain",
}
ZERO_ADDR = "0x0000000000000000000000000000000000000000"
def _dexscreener_price(chain, token_addr):
"""Liquidity-weighted price from DexScreener (experimental price fallback)."""
try:
resp = requests.get(f"https://api.dexscreener.com/latest/dex/tokens/{token_addr}", timeout=2)
if resp.status_code != 200:
return None
pairs = resp.json().get("pairs", [])
wp, tl = 0.0, 0.0
for pair in pairs:
liq = (pair.get("liquidity") or {}).get("usd", 0)
pu = pair.get("priceUsd")
ba = (pair.get("baseToken") or {}).get("address", "")
if liq > 10000 and pu and ba.lower() == token_addr.lower():
p = float(pu)
if tl > 0:
avg = wp / tl
if abs(p - avg) / avg > 0.1:
continue
wp += p * liq
tl += liq
return wp / tl if tl > 0 else None
except Exception:
return None
def get_fair_market_prices(chain, from_addr, to_addr):
"""Fetch fair market prices from DefiLlama coins API + DexScreener fallback."""
llama_chain = DEFILLAMA_CHAIN_MAP.get(chain, chain)
coin_ids = [f"{llama_chain}:{ZERO_ADDR}"]
if from_addr != ZERO_ADDR:
coin_ids.append(f"{llama_chain}:{from_addr}")
if to_addr != ZERO_ADDR:
coin_ids.append(f"{llama_chain}:{to_addr}")
prices = {}
try:
resp = requests.get(f"https://coins.llama.fi/prices/current/{','.join(coin_ids)}", timeout=5)
if resp.status_code == 200:
for cid, data in resp.json().get("coins", {}).items():
prices[cid.split(":")[-1].lower()] = data.get("price")
except Exception:
pass
for addr in [from_addr, to_addr]:
if addr != ZERO_ADDR and (addr.lower() not in prices or prices.get(addr.lower()) is None):
p = _dexscreener_price(chain, addr)
if p:
prices[addr.lower()] = p
return {
"gas_token_price": prices.get(ZERO_ADDR),
"from_token_price": prices.get(ZERO_ADDR if from_addr == ZERO_ADDR else from_addr.lower()),
"to_token_price": prices.get(ZERO_ADDR if to_addr == ZERO_ADDR else to_addr.lower()),
}
def calculate_price_impact(amount_in, amount_out, from_price, to_price):
if not all([from_price, to_price, amount_in, amount_out]):
return None
val_in = float(amount_in) * float(from_price)
val_out = float(amount_out) * float(to_price)
if val_in <= 0:
return None
return round(((val_in - val_out) / val_in) * 100, 4)
def classify_price_impact(impact):
if impact is None: return "unknown"
if impact < PRICE_IMPACT_WARNING: return "ok"
if impact < PRICE_IMPACT_MEDIUM: return "warning"
if impact < PRICE_IMPACT_HIGH: return "high"
return "critical"
def slippage_warnings(slippage, from_sym, to_sym):
warnings = []
s = float(slippage)
is_stable = from_sym in STABLECOINS and to_sym in STABLECOINS
if s > SLIPPAGE_SANDWICH_RISK:
warnings.append({"level": "warning", "msg": f"High slippage ({s}%)! You might get sandwiched."})
if is_stable and s > STABLECOIN_SLIPPAGE_MAX:
warnings.append({"level": "warning", "msg": f"Stablecoin pair but slippage is {s}% — recommend ≤0.05%."})
if not is_stable and s < LOW_SLIPPAGE_THRESHOLD:
warnings.append({"level": "warning", "msg": f"Slippage very low ({s}%) — tx likely to revert."})
return warnings
def gas_adjusted_ranking(quotes, gas_token_price, to_token_price):
"""Rank quotes by net output (amountUsd - gasUsd) with gas-adjusted comparison."""
ranked = []
for q in quotes:
out = float(q.get("outputAmount", 0))
gas_est = q.get("estimatedGas", 0)
amt_usd = out * float(to_token_price) if to_token_price else None
gas_usd = None
if gas_est and gas_token_price:
gas_usd = float(gas_est) * 30 * 1e-9 * float(gas_token_price)
if amt_usd is not None and gas_usd is not None:
net = amt_usd - gas_usd
elif amt_usd is not None:
net = amt_usd
else:
net = out
ranked.append({
**q,
"amountUsd": round(amt_usd, 2) if amt_usd else None,
"gasUsd": round(gas_usd, 4) if gas_usd else None,
"netOut": round(net, 4),
"isMEVSafe": q.get("name", "") in MEV_SAFE_AGGREGATORS,
})
ranked.sort(key=lambda x: x["netOut"], reverse=True)
if ranked:
best = ranked[0]["netOut"]
for r in ranked:
r["vsbestPct"] = round((r["netOut"] / best * 100) if best > 0 else 100, 2)
r["isOutlier"] = r["vsbestPct"] < 95
return ranked
def full_safety_check(quotes, chain, from_addr, to_addr, from_sym, to_sym,
amount_in_human, slippage="0.5"):
"""Complete safety analysis pipeline."""
prices = get_fair_market_prices(chain, from_addr, to_addr)
ranked = gas_adjusted_ranking(quotes, prices["gas_token_price"], prices["to_token_price"])
best = ranked[0] if ranked else None
impact = None
severity = "unknown"
if best and prices["from_token_price"] and prices["to_token_price"]:
impact = calculate_price_impact(
amount_in_human, best.get("outputAmount", 0),
prices["from_token_price"], prices["to_token_price"]
)
severity = classify_price_impact(impact)
slip_w = slippage_warnings(slippage, from_sym, to_sym)
# Build recommendation
rec = []
if severity == "critical":
rec.append(f"⛔ PRICE IMPACT {impact:.1f}% — You will likely lose money.")
elif severity == "high":
rec.append(f"🔴 HIGH PRICE IMPACT ({impact:.1f}%) — Proceed with extreme caution.")
elif severity == "warning":
rec.append(f"🟡 Moderate price impact ({impact:.1f}%) — Review carefully.")
elif severity == "unknown":
rec.append("⚠️ Could not determine price impact (no market price available).")
# MEV recommendation
mev_safe = [r for r in ranked if r.get("isMEVSafe")]
if mev_safe and ranked and not ranked[0].get("isMEVSafe"):
bm = mev_safe[0]
if ranked[0]["netOut"] > 0:
d = abs(ranked[0]["netOut"] - bm["netOut"]) / ranked[0]["netOut"] * 100
if d < 0.5:
rec.append(f"🛡️ {bm['name']} is MEV-protected and only {d:.2f}% worse — recommended.")
elif d < 2:
rec.append(f"🛡️ {bm['name']} offers MEV protection ({d:.1f}% less output).")
for w in slip_w:
rec.append(f"⚠️ {w['msg']}")
outliers = [r["name"] for r in ranked if r.get("isOutlier")]
if outliers:
rec.append(f"🚫 Outlier quotes (>5% worse): {', '.join(outliers)} — avoid.")
if not rec:
rec.append("✅ All checks passed. Best route looks safe.")
return {
"prices": prices,
"ranked_quotes": ranked,
"price_impact": {"value": impact, "severity": severity},
"slippage_warnings": slip_w,
"recommendation": " | ".join(rec),
}
if __name__ == "__main__":
prices = get_fair_market_prices("ethereum", ZERO_ADDR, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
print(f"Gas token (ETH): ${prices['gas_token_price']}")
print(f"From (ETH): ${prices['from_token_price']}")
print(f"To (USDC): ${prices['to_token_price']}")
impact = calculate_price_impact(1.0, 2100, prices["from_token_price"], prices["to_token_price"])
print(f"\n1 ETH -> 2100 USDC: impact {impact:.2f}% ({classify_price_impact(impact)})")
impact2 = calculate_price_impact(1.0, 1800, prices["from_token_price"], prices["to_token_price"])
print(f"1 ETH -> 1800 USDC: impact {impact2:.2f}% ({classify_price_impact(impact2)})")
print(f"\nSlippage warnings (2% volatile): {slippage_warnings('2', 'ETH', 'USDC')}")
print(f"Slippage warnings (1% stables): {slippage_warnings('1', 'USDC', 'USDT')}")
"""Token list resolution for Meta DEX Aggregator.
Three-tier resolution:
1. Trusted majors — hardcoded canonical addresses, auto-resolved (no confirmation)
2. Single match — only one token matches the symbol, auto-resolved
3. Ambiguous — multiple matches, returns candidates for user confirmation
"""
import requests
from decimal import Decimal
from chains import CHAINS, ZERO_ADDR, NATIVE_SYMBOLS
# ── Tier 1: Trusted canonical addresses per chain ──────────────────────
# These are THE canonical tokens — never need confirmation.
# Format: { chain_id: { "SYMBOL": ("address", decimals) } }
TRUSTED_TOKENS = {
# Ethereum (1)
1: {
"WETH": ("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 18),
"USDC": ("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 6),
"USDT": ("0xdAC17F958D2ee523a2206206994597C13D831ec7", 6),
"DAI": ("0x6B175474E89094C44Da98b954EedeAC495271d0F", 18),
"WBTC": ("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", 8),
"LINK": ("0x514910771AF9Ca656af840dff83E8264EcF986CA", 18),
"UNI": ("0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", 18),
"AAVE": ("0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9", 18),
"MKR": ("0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", 18),
"STETH": ("0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", 18),
"WSTETH":("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", 18),
"CBETH": ("0xBe9895146f7AF43049ca1c1AE358B0541Ea49704", 18),
"RETH": ("0xae78736Cd615f374D3085123A210448E74Fc6393", 18),
"RPL": ("0xD33526068D116cE69F19A9ee46F0bd304F21A51f", 18),
"LDO": ("0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", 18),
"CRV": ("0xD533a949740bb3306d119CC777fa900bA034cd52", 18),
"COMP": ("0xc00e94Cb662C3520282E6f5717214004A7f26888", 18),
"SNX": ("0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F", 18),
"PEPE": ("0x6982508145454Ce325dDbE47a25d4ec3d2311933", 18),
"SHIB": ("0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE", 18),
},
# Arbitrum (42161)
42161: {
"WETH": ("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", 18),
"USDC": ("0xaf88d065e77c8cC2239327C5EDb3A432268e5831", 6), # native USDC
"USDC.e":("0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", 6), # bridged
"USDT": ("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", 6),
"DAI": ("0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", 18),
"WBTC": ("0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f", 8),
"ARB": ("0x912CE59144191C1204E64559FE8253a0e49E6548", 18),
"LINK": ("0xf97f4df75117a78c1A5a0DBb814Af92458539FB4", 18),
"UNI": ("0xFa7F8980b0f1E64A2062791cc3b0871572f1F7f0", 18),
"GMX": ("0xfc5A1A6EB076a2C7aD06eD22C90d7E710E35ad0a", 18),
"PENDLE":("0x0c880f6761F1af8d9Aa9C466984b80DAb9a8c9e8", 18),
"GRT": ("0x9623063377AD1B27544C965cCd7342f7EA7e88C7", 18),
"RDNT": ("0x3082CC23568eA640225c2467653dB90e9250AaA0", 18),
"WSTETH":("0x5979D7b546E38E9Ab8049e39eAcB0B30261edE29", 18),
"RETH": ("0xEC70Dcb4A1EFa46b8F2D97C310C9c4790ba5ffA8", 18),
},
# Base (8453)
8453: {
"WETH": ("0x4200000000000000000000000000000000000006", 18),
"USDC": ("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6),
"USDBC": ("0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA", 6), # bridged
"DAI": ("0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", 18),
"CBETH": ("0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22", 18),
"WSTETH":("0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", 18),
"AERO": ("0x940181a94A35A4569E4529A3CDfB74e38FD98631", 18),
"DEGEN": ("0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed", 18),
"BRETT": ("0x532f27101965dd16442E59d40670FaF5eBB142E4", 18),
"TOSHI": ("0xAC1Bd2486aAf3B5C0fc3Fd868558b082a531B2B4", 18),
},
# Optimism (10)
10: {
"WETH": ("0x4200000000000000000000000000000000000006", 18),
"USDC": ("0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", 6), # native
"USDC.e":("0x7F5c764cBc14f9669B88837ca1490cCa17c31607", 6), # bridged
"USDT": ("0x94b008aA00579c1307B0EF2c499aD98a8ce58e58", 6),
"DAI": ("0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", 18),
"WBTC": ("0x68f180fcCe6836688e9084f035309E29Bf0A2095", 8),
"OP": ("0x4200000000000000000000000000000000000042", 18),
"LINK": ("0x350a791Bfc2C21F9Ed5d10980Dad2e2638ffa7f6", 18),
"SNX": ("0x8700dAec35aF8Ff88c16BdF0418774CB3D7599B4", 18),
"WSTETH":("0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb", 18),
"RETH": ("0x9Bcef72bE871e61ED4fBbc7630889beE758eb81D", 18),
"VELO": ("0x9560e827aF36c94D2Ac33a39bCE1Fe78631088Db", 18),
},
# Polygon (137)
137: {
"WETH": ("0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", 18),
"WMATIC":("0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", 18),
"WPOL": ("0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", 18),
"USDC": ("0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", 6), # native
"USDC.e":("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", 6), # bridged
"USDT": ("0xc2132D05D31c914a87C6611C10748AEb04B58e8F", 6),
"DAI": ("0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", 18),
"WBTC": ("0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", 8),
"LINK": ("0x53E0bca35eC356BD5ddDFebbD1Fc0fD03FaBad39", 18),
"AAVE": ("0xD6DF932A45C0f255f85145f286eA0b292B21C90B", 18),
"UNI": ("0xb33EaAd8d922B1083446DC23f610c2567fB5180f", 18),
},
# BSC (56)
56: {
"WBNB": ("0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", 18),
"USDC": ("0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", 18),
"USDT": ("0x55d398326f99059fF775485246999027B3197955", 18),
"DAI": ("0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3", 18),
"WETH": ("0x2170Ed0880ac9A755fd29B2688956BD959F933F8", 18),
"BTCB": ("0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c", 18),
"CAKE": ("0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82", 18),
"XRP": ("0x1D2F0da169ceB9fC7B3144628dB156f3F6c60dBE", 18),
},
# Avalanche (43114)
43114: {
"WAVAX": ("0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", 18),
"USDC": ("0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", 6),
"USDT": ("0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", 6),
"DAI.e": ("0xd586E7F844cEa2F87f50152665BCbc2C279D8d70", 18),
"WETH.e":("0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB", 18),
"WBTC.e":("0x50b7545627a5162F82A992c33b87aDc75187B218", 8),
"JOE": ("0x6e84a6216eA6dACC71eE8E6b0a5B7322EEbC0fDd", 18),
},
# Gnosis (100)
100: {
"WXDAI": ("0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d", 18),
"USDC": ("0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83", 6),
"USDT": ("0x4ECaBa5870353805a9F068101A40E0f32ed605C6", 6),
"WETH": ("0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1", 18),
"GNO": ("0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb", 18),
},
}
# ── Token cache ────────────────────────────────────────────────────────
_token_cache = {}
def get_token_list(chain_id: int) -> dict:
if chain_id in _token_cache:
return _token_cache[chain_id]
url = f"https://d3g10bzo9rdluh.cloudfront.net/tokenlists-{chain_id}.json"
resp = requests.get(url, timeout=15)
resp.raise_for_status()
data = resp.json()
_token_cache[chain_id] = data
return data
def search_tokens(chain: str, query: str, limit: int = 20):
chain_id = CHAINS[chain]
tokens = get_token_list(chain_id)
query_lower = query.lower()
results = []
for addr, tok in tokens.items():
if query_lower in tok.get("symbol", "").lower() or query_lower in tok.get("name", "").lower():
results.append(tok)
results.sort(key=lambda t: t.get("volume24h", 0) or 0, reverse=True)
return results[:limit]
def resolve_token(chain: str, symbol_or_address: str) -> dict:
"""Resolve symbol or address to { address, decimals, symbol, confidence }.
confidence levels:
"trusted" — hardcoded canonical address, no confirmation needed
"exact" — user provided a 0x address directly
"single" — only one symbol match found, safe to auto-use
"high" — multiple matches but top result has >>10x volume of #2
"ambiguous" — multiple plausible matches, needs user confirmation
When confidence is "ambiguous", the returned dict includes a "candidates"
list with the top matches so the agent can present them to the user.
"""
chain_id = CHAINS[chain]
native_sym = NATIVE_SYMBOLS.get(chain, "ETH")
sym_upper = symbol_or_address.strip().upper()
# ── Native gas token ──
if sym_upper in (native_sym, "ETH") and native_sym == "ETH":
return {"address": ZERO_ADDR, "decimals": 18, "symbol": native_sym, "confidence": "trusted"}
if sym_upper == native_sym:
return {"address": ZERO_ADDR, "decimals": 18, "symbol": native_sym, "confidence": "trusted"}
# ── User gave a 0x address directly ──
if symbol_or_address.startswith("0x") and len(symbol_or_address) == 42:
tokens = get_token_list(chain_id)
tok = tokens.get(symbol_or_address.lower())
if tok:
return {"address": tok["address"], "decimals": tok["decimals"],
"symbol": tok["symbol"], "confidence": "exact"}
return {"address": symbol_or_address.lower(), "decimals": 18,
"symbol": "UNKNOWN", "confidence": "exact"}
# ── Tier 1: Trusted canonical tokens ──
trusted = TRUSTED_TOKENS.get(chain_id, {})
if sym_upper in trusted:
addr, dec = trusted[sym_upper]
return {"address": addr, "decimals": dec, "symbol": sym_upper, "confidence": "trusted"}
# ── Tier 2+3: Search token list by symbol ──
tokens = get_token_list(chain_id)
matches = [tok for addr, tok in tokens.items()
if tok.get("symbol", "").upper() == sym_upper]
if not matches:
raise ValueError(f"Token '{symbol_or_address}' not found on {chain}. Use 'search' command.")
matches.sort(key=lambda t: t.get("volume24h", 0) or 0, reverse=True)
# Single match — no ambiguity
if len(matches) == 1:
best = matches[0]
return {"address": best["address"], "decimals": best["decimals"],
"symbol": best["symbol"], "confidence": "single"}
# Multiple matches — check if top is dominant (10x volume of runner-up)
top_vol = matches[0].get("volume24h", 0) or 0
second_vol = matches[1].get("volume24h", 0) or 0
if top_vol > 0 and (second_vol == 0 or top_vol / max(second_vol, 1) > 10):
best = matches[0]
return {"address": best["address"], "decimals": best["decimals"],
"symbol": best["symbol"], "confidence": "high"}
# Ambiguous — return candidates for confirmation
candidates = []
for m in matches[:5]:
candidates.append({
"symbol": m["symbol"],
"name": m.get("name", ""),
"address": m["address"],
"decimals": m["decimals"],
"volume24h": m.get("volume24h"),
})
best = matches[0]
return {"address": best["address"], "decimals": best["decimals"],
"symbol": best["symbol"], "confidence": "ambiguous",
"candidates": candidates}
def to_wei(amount: str, decimals: int) -> str:
return str(int(Decimal(amount) * Decimal(10 ** decimals)))
def from_wei(amount_wei: str, decimals: int) -> str:
return str(Decimal(str(amount_wei)) / Decimal(10 ** decimals))