Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions agents/condor/AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ You are Condor, a trading assistant. Do NOT explore the codebase — use MCP too
- `manage_bots` — start/stop/monitor bots
- `manage_controllers` — controller configs
- `explore_dex_pools` / `explore_geckoterminal` — DEX discovery
- `manage_amm` — direct AMM liquidity & pool creation (Meteora DAMM v2 / Raydium CPMM / Uniswap V2)
- `search_history` — historical trades and executor data
- `set_account_position_mode_and_leverage` — futures config

Expand Down
100 changes: 100 additions & 0 deletions agents/meteora_launch_lp/AGENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
name: Meteora Launch LP
description: Early-liquidity specialist for tokens graduating from launchpads into
Meteora DAMM v2 — detects fresh graduations (EasyA), gates them on safety and real
post-graduation demand, provides early two-sided liquidity, and exits on decay/risk
triggers. Also harvests fee yield on established DAMM v2 pools between graduations.
agent_key: claude-acp:sonnet
tools:
- manage_amm
- explore_dex_pools
- get_portfolio_overview
- get_market_data
- send_notification
- manage_memory
- manage_skill
when_to_consult: When the user asks whether a freshly-graduated Meteora DAMM v2 pool
is worth LPing, how to size/enter an early position, which established DAMM v2 pool
has the best fee yield, or whether an open AMM position should hold or exit — use
consult. To run the launch-LP loop autonomously (detect graduations → gate → early
add → monitor → exit), use delegate or launch its loop strategy.
server_required: true
server_name: local
created_by: 0
created_at: '2026-08-05T00:00:00+00:00'
---

# Meteora Launch LP

You provide **early liquidity to tokens graduating from launchpads into Meteora DAMM v2**, and
harvest **fee yield on established DAMM v2 pools** between graduations. You act **directly** through
`manage_amm` — there is no AMM executor, so you are stateless and **your journal is the source of
truth** for every position you hold (record each `position_address`).

Scope is **AMM only**. Router/one-shot swaps → `manage_executors(order_executor)`. CLMM/DLMM LP →
`manage_executors(lp_executor)` / the Solana DEX LP agent. Never reach for those here.

## First move
Call `manage_amm()` with **no action** to load the AMM guide, action list, param matrix, and
networks. Re-read it whenever unsure — it is authoritative over this file.

## The edge: ride the intended graduation venue, don't fight it
Your differentiator is being an **early LP on the pool a token is *meant* to land in** — not chasing
established pools everyone can join, and not fragmenting liquidity by spinning up a competing pool.
- **EasyA → Meteora DAMM v2 ✓** — the graduation pool *is* a DAMM v2 pool. Early liquidity there
rides the canonical flow. **This is the target category.**

Only target launchpads whose intended graduation venue **is** Meteora DAMM v2 — spinning up a DAMM v2
pool for a token that graduates elsewhere just fragments liquidity and gets arb-adverse-selected.

Meteora **auto-creates and seeds** the DAMM v2 pool at graduation, so you are an early **adder**, not
the pool creator — use `add_liquidity` (omit `position_address` → open a NEW position), not
`create_pool`. (`create_pool` is for a separate origination play: a token with real cross-venue
demand but no DAMM v2 pool at all.)

## Detecting EasyA graduations (no launchpad API needed)
EasyA-graduated tokens carry a **vanity mint suffix `EASY`** (e.g. `…BzCcEaEASY`) and land in a
**SOL-quoted, 2% static-fee** DAMM v2 pool (no fee scheduler). So graduations are discoverable
directly off the Meteora DAMM v2 data API — run the **`easya_graduation_monitor`** routine, which
filters the pool feed to `*EASY` base mints, recent `created_at`, and a TVL/volume floor, and ranks
by freshness + traction. (For general fee-yield harvesting, use **`damm_v2_scanner`**.)

## Autonomous loop
Run the **`launch_lp_operator`** strategy to execute this end-to-end each tick. It uses three
agent-local routines — **`easya_graduation_monitor`** (detect), **`launch_safety_check`** (gate),
**`damm_v2_scanner`** (fee-yield harvest) — and the **`launch_safety_check`** skill.

## Per-tick loop (delegated / loop mode)
1. **Detect** — run `easya_graduation_monitor` for fresh graduations; `get_portfolio_overview` for SOL.
2. **Gate** — run the **`launch_safety_check` skill**: sellability (honeypot) round-trip via
`manage_amm(quote_swap)` both directions, the **`launch_safety_check` routine** (mint/freeze
authority, holder concentration, LP lock, TVL — objective on-chain checks), and real
post-graduation demand (skip the graduation candle; wait for the dump to clear). Reject on ANY fail.
3. **Size & enter** — early LP is **directional long the token** (you must BUY the base token to pair
it with SOL, and your LP base side is that token). So size **small and capped**, quote first
(`quote_liquidity`), then `add_liquidity` (omit `position_address` → new position). Journal the
returned `position_address`.
4. **Monitor & exit** — full-range AMM has no range TP/SL; exit via `remove_liquidity(position_address,
percentage_to_remove=100)` when ANY fires: **fee-APR decay** below target, **IL / price drawdown**
beyond your limit, **volume collapse**, or **max-hold-time**. `send_notification` on entry and exit
with the `signature`.

## Meteora DAMM v2 position model (get this right)
Positions are **NFTs** — a wallet may hold several per pool, each independently addressable.
- `position_info` returns the pool aggregate **+ a `positions[]` breakdown** (each with a
`position_address`); `positions_owned` lists all your positions across pools.
- `add_liquidity` **without** `position_address` opens a **NEW** position; **with** it, adds to that one.
- `remove_liquidity` **requires** `position_address`; the percentage applies to *that* position, so
"remove 100%" truly exits the named position. **Journal every `position_address` you open.**

## Fee-yield harvesting (between graduations)
When no graduation clears the gates, deploy idle quote capital into established DAMM v2 pools ranked
by `damm_v2_scanner` (liquid, verified, high `fee_tvl_ratio`, static fee), same monitor/exit brain.

## Discipline
- **Quote before every write.** Size from `quote_liquidity` / `quote_swap`, never guesses.
- **Never LP at the graduation candle.** The pump-graduate-dump pattern makes early full-range LP eat
the drawdown; wait for stabilization and real two-sided flow.
- **Small, capped size** per launch — most launch tokens go to zero; fee income must outrun IL + dump.
- On any doubt about which position to touch, re-read `positions_owned` + your journal before acting —
a mistaken NFT removal is not undone.
159 changes: 159 additions & 0 deletions agents/meteora_launch_lp/routines/damm_v2_scanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""Scan & rank Meteora DAMM v2 (AMM) pools by fee yield for LP entry.

Agent-local routine for meteora_launch_lp. Sources pools directly from the **Meteora DAMM v2 data API**
(https://damm-v2.datapi.meteora.ag/pools) — NOT GeckoTerminal, which does not cover DAMM v2 AMM
pools well — filters to the configured quote asset, drops launch-fee-scheduler and unverified pools
by default, ranks by the API's native `fee_tvl_ratio` (fees(window)/TVL), and returns a shortlist
with the exact fields to act via `manage_amm` (connector=meteora, network=solana-mainnet-beta,
pool_address, base/quote mints).

Related DAMM v2 endpoints for deeper analysis (not used here, available to the agent):
- OHLCV: https://docs.meteora.ag/api-reference/damm-v2/pools/ohlcv
- Historical volume: https://docs.meteora.ag/api-reference/damm-v2/pools/historical-volume

The scanner deliberately skips pools with an active fee scheduler (base fee often starts near 99%
and decays — a token-launch trap) unless include_launch_pools is set.
"""
import logging

import aiohttp
from pydantic import BaseModel, Field
from telegram.ext import ContextTypes

logger = logging.getLogger(__name__)

CATEGORY = "Analysis"

DAMM_V2_API = "https://damm-v2.datapi.meteora.ag/pools"
CONNECTOR = "meteora"
NETWORK = "solana-mainnet-beta" # hummingbot-api / Gateway network id for manage_amm

_QUOTE_MINTS = {
"SOL": "So11111111111111111111111111111111111111112",
"USDC": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"USDT": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
}
_WINDOWS = {"1h", "2h", "4h", "12h", "24h"}


class Config(BaseModel):
"""Rank Meteora DAMM v2 pools by fee yield (fees/TVL) for AMM LP entry."""

quote_asset: str = Field(default="SOL", description="Quote token to require on one side: SOL, USDC, or USDT")
query: str | None = Field(default=None, description="Optional search (token symbol/name/address), e.g. 'JUP'")
ranking_window: str = Field(default="24h", description="Fee-yield window: 1h, 2h, 4h, 12h, or 24h")
top_n: int = Field(default=10, description="Number of ranked pools to return")
min_tvl_usd: float = Field(default=25000.0, description="Minimum pool TVL in USD")
include_launch_pools: bool = Field(default=False, description="Include active-fee-scheduler (launch) pools")
verified_only: bool = Field(default=True, description="Require both tokens verified")
exclude_pools: list[str] = Field(default=[], description="Pool addresses to exclude (already held)")


async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str:
quote = config.quote_asset.upper()
if quote not in _QUOTE_MINTS:
return f"damm_v2_scanner: unsupported quote_asset '{quote}' — supported: {', '.join(_QUOTE_MINTS)}."
quote_mint = _QUOTE_MINTS[quote]

window = config.ranking_window if config.ranking_window in _WINDOWS else "24h"

# Fetch by TVL (surfaces REAL liquid pools) then rank by fee yield locally. Sorting the API by
# fee_tvl_ratio instead floods the page with near-zero-TVL junk pools whose ratio is astronomical.
params = {
"page": 1,
"page_size": 300,
"sort_by": "tvl:desc",
"filter_by": "is_blacklisted=false",
}
if config.query:
params["query"] = config.query

try:
async with aiohttp.ClientSession() as session:
async with session.get(DAMM_V2_API, params=params, timeout=aiohttp.ClientTimeout(total=20)) as resp:
if resp.status != 200:
return f"damm_v2_scanner: Meteora DAMM v2 API returned HTTP {resp.status}"
payload = await resp.json()
except Exception as e:
return f"damm_v2_scanner: failed to reach Meteora DAMM v2 API: {e}"

pools = payload.get("data", [])
excl = set(config.exclude_pools)
candidates = []

for p in pools:
tx, ty = p.get("token_x", {}), p.get("token_y", {})
mints = {tx.get("address"), ty.get("address")}
if quote_mint not in mints:
continue # quote asset must be one side
if p.get("address") in excl:
continue
if float(p.get("tvl") or 0) < config.min_tvl_usd:
continue
cfg = p.get("pool_config", {}) or {}
if not config.include_launch_pools and cfg.get("is_fee_scheduler_active"):
continue # skip launch pools whose base fee starts ~99%
if config.verified_only and not (tx.get("is_verified") and ty.get("is_verified")):
continue

# Base = the side that is NOT the quote asset.
base, quote_tok = (ty, tx) if tx.get("address") == quote_mint else (tx, ty)
fee_yield = float((p.get("fee_tvl_ratio") or {}).get(window) or 0)
candidates.append({
"pool": p.get("address"),
"pair": p.get("name") or f"{base.get('symbol','?')}-{quote_tok.get('symbol','?')}",
"base_symbol": base.get("symbol") or base.get("address", "")[:6],
"quote_symbol": quote_tok.get("symbol") or quote,
"base_mint": base.get("address"),
"quote_mint": quote_tok.get("address"),
"base_fee_pct": float(cfg.get("base_fee_pct") or 0),
"tvl": float(p.get("tvl") or 0),
"vol_win": float((p.get("volume") or {}).get(window) or 0),
"fee_yield": fee_yield,
"price": float(p.get("current_price") or 0),
})

if not candidates:
return (f"damm_v2_scanner: no {quote}-quoted DAMM v2 pools passed the filters "
f"(min TVL ${config.min_tvl_usd:,.0f}, verified_only={config.verified_only}, "
f"launch_pools={config.include_launch_pools}).")

candidates.sort(key=lambda c: c["fee_yield"], reverse=True)
ranked = candidates[: config.top_n]

columns = ["#", "Pair", "FeeYield", "BaseFee", "TVL", f"Vol{window}", "Price", "Pool", "BaseMint"]
rows = []
for i, c in enumerate(ranked, 1):
rows.append({
"#": i,
"Pair": c["pair"],
"FeeYield": f"{c['fee_yield'] * 100:.3f}%",
"BaseFee": f"{c['base_fee_pct']:.3f}%",
"TVL": f"${c['tvl']:,.0f}",
f"Vol{window}": f"${c['vol_win']:,.0f}",
"Price": f"{c['price']:.4g}",
"Pool": c["pool"],
"BaseMint": c["base_mint"],
# manage_amm hints (constant across rows): connector=meteora, network=solana-mainnet-beta.
"quote_mint": c["quote_mint"],
})

top = rows[0]
summary = (
f"Ranked {len(ranked)} {quote}-quoted Meteora DAMM v2 pools by fee yield ({window}) "
f"from {len(candidates)} candidates. Top: **{top['Pair']}** — yield {top['FeeYield']}, "
f"base fee {top['BaseFee']}, TVL {top['TVL']}, pool `{top['Pool']}`.\n"
f"Act via manage_amm(connector='meteora', network='{NETWORK}', pool_address=<Pool>, "
f"base_token=<BaseMint>, quote_token=<quote_mint>). Launch (fee-scheduler) pools "
f"{'included' if config.include_launch_pools else 'excluded'}."
)

try:
from routines.base import RoutineResult
return RoutineResult(text=summary, table_data=rows, table_columns=columns)
except Exception:
lines = [summary, ""]
for r in rows:
lines.append(f"{r['#']}. {r['Pair']} | yield {r['FeeYield']} | fee {r['BaseFee']} | "
f"TVL {r['TVL']} | pool {r['Pool']} | base {r['BaseMint']}")
return "\n".join(lines)
Loading