Autonomous Meteora DLMM liquidity management agent for Solana.
Meridian screens pools, deploys capital, manages open positions, records lessons, and can evolve both thresholds and prompt behavior over time. It is designed to run continuously with a web dashboard, REPL, and optional Telegram control surface.
Meridian has three LLM-facing agent roles:
| Agent | Purpose | Typical schedule |
|---|---|---|
SCREENER |
Finds candidates, studies pools, decides whether to deploy | every screeningIntervalMin |
MANAGER |
Reviews live positions, claims fees, closes or holds | every managementIntervalMin |
GENERAL |
Chat and command handling | on demand |
There is also an autoresearch subsystem that tests prompt changes after real closes and keeps or reverts them based on later performance.
Meridian supports five provider modes. Set via LLM_PROVIDER in .env or llmProvider in user-config.json:
| Provider | How it works | Auth | Cost |
|---|---|---|---|
claude |
Runs model turns through claude -p (Claude Code CLI) |
OAuth login (claude CLI) |
Uses your Claude Pro/Max subscription |
codex |
Runs model turns through codex exec (Codex CLI) |
OAuth login (codex login) |
Uses your Codex/OpenAI subscription |
openrouter |
Direct HTTP API calls to any model | OPENROUTER_API_KEY |
Pay-per-token via OpenRouter |
deepseek |
Direct HTTP API calls | DEEPSEEK_API_KEY |
Pay-per-token via DeepSeek |
minimax |
Direct HTTP API calls to MiniMax's OpenAI-compatible API | MINIMAX_API_KEY |
Uses your MiniMax Token Plan or pay-as-you-go key |
Uses claude -p (Claude Code print mode) with your existing Claude subscription. No API key billing β runs on your OAuth login. Supports per-role model selection:
{
"llmProvider": "claude",
"screeningModel": "opus",
"managementModel": "haiku",
"generalModel": "sonnet",
"autoresearchModel": "opus"
}Available model aliases: opus (Opus 4.6), sonnet (Sonnet 4.6), haiku (Haiku 4.5).
Uses codex exec with your OpenAI/Codex subscription. Same CLI harness pattern as Claude but with OpenAI models.
Uses the OpenRouter API to access any model (minimax, qwen, etc.). Requires OPENROUTER_API_KEY in .env. Good for cheap models like minimax/minimax-m2.7 or free models like qwen/qwen3.6-plus:free.
Uses MiniMax's OpenAI-compatible API directly at https://api.minimax.io/v1. This is the right option if you want to use a MiniMax Token Plan key directly instead of routing MiniMax through OpenRouter.
Typical models:
MiniMax-M2.7MiniMax-M2.7-highspeedMiniMax-M2.5
All providers use the same ReAct loop with your custom tools β the provider only affects which LLM processes the prompts.
LLM provider -> ReAct loop -> tools -> Meteora / Helius / Jupiter / LP Agent
| |
| +-- wallet, pools, token info, deploy, close, swap
|
+-- Screener
+-- Manager
+-- General chat
+-- Autoresearch prompt optimizer
- Node.js 18+
- A Solana wallet private key in base58 format
- A Solana RPC URL, ideally Helius
- Codex CLI login if using
codex - LP Agent API key if you want LP overview / top LPer study
- Telegram bot token if you want Telegram control and notifications
git clone https://github.com/fciaf420/meridian.git
cd meridian
npm install
cd web && npm install && npm run build && cd ..Choose your provider:
Claude (recommended β uses your Claude subscription):
# Make sure claude CLI is installed and logged in
claude --versionCodex (uses your OpenAI/Codex subscription):
codex loginOpenRouter (pay-per-token, any model):
Add OPENROUTER_API_KEY=sk-or-... to .env
MiniMax Token Plan (direct MiniMax access):
Add MINIMAX_API_KEY=... to .env
Then run:
npm run setupThe setup wizard saves a starter user-config.json. It asks for:
- wallet and RPC
- risk preset
- deploy size and limits
- screening and management cadence
- provider selection
- a default model ID
- dry-run vs live mode
Before live trading:
npm run devThis runs the full bot with DRY_RUN=true. The agent still screens, manages, chats, updates the dashboard, and runs cron jobs, but real transactions are not broadcast.
When you are satisfied:
npm startThis is live mode.
Before a real run, verify:
dryRuninuser-config.jsonis what you expect- wallet key is set
- RPC URL works
llmProvideris correct- if using
codex,codex loginhas already been done on this machine - the dashboard loads
- the first screening and management cycles complete without errors
- Telegram is registered if you enabled it
Meridian has a lot of knobs, but you only need to look at a few.
| Tier | What | Where |
|---|---|---|
| 0. Secrets | WALLET_PRIVATE_KEY, RPC_URL, TELEGRAM_BOT_TOKEN (required); LLM key and GMGN_API_KEY (optional) |
.env only. Never shown or accepted in Telegram. |
| 1. Basics | dry run, profile, max positions, position size, max SOL per position, screening source, strategy + shape, LLM provider | Telegram: Menu β Settings console (or user-config.json) |
| 2. Tuning | ~25 knobs a profile sets for you: exits, screening thresholds, entry filters, sizing floor, cadence | Console β Tuning |
| 3. Advanced | everything else (GMGN filters, per-role models, Darwin/autoresearch constants, USDC mode, ...) | Console β Advanced (the All-settings editor), or user-config.json |
The list of settings, with type, range, unit, tier, restart-required and profile-governed flags, is one table: settings-schema.js. config.js, the Telegram console and the All-settings editor are all generated from it.
"profile" in user-config.json (or the console's Profile picker) is one of:
balanced: the values this bot has been running live.conservative: tighter exits, stricter screening, fewer and smaller positions. Untested starting point.degen: wider exits, looser screening, more positions. Untested starting point.custom: no preset;user-config.jsonis the whole truth. A config from before profiles has noprofilekey and loads ascustomwith an identical effective config.
Resolution for every setting: user-config.json value > profile preset > schema default. Setting a Tuning knob yourself under a profile is an override (shown with β in the console) and survives restarts. Switching profile shows a diff and asks for confirmation; it drops your Tuning overrides so the preset takes effect. Switching to custom pins every Tuning knob to its current value (no behavior change).
- Secrets:
.envwins. AwalletKey/rpcUrl/gmgnApiKeyleft inuser-config.json/gmgn-config.jsonstill works for one more release and logs a deprecation warning. - Everything else:
user-config.jsonwins over a stale.envvalue. The old env namesDRY_RUN,USDC_MODE,LLM_PROVIDER,LLM_MODEL,WEB_PORTare only a fallback whenuser-config.jsonhas no value, and log a deprecation warning (and a loud "ignored" warning when they disagree). - Per-role models:
managementModel/screeningModel/generalModel, thenllmModel, then the provider default.
autoTune (default false) controls whether lessons.js may rewrite thresholds in user-config.json after closes. Off, it still runs the analysis but only logs (evolve_proposed) and records the latest proposal in config-state.json. Explicit owner commands (/evolve, dashboard) still apply immediately. Bookkeeping counters (_lastEvolved etc.) live in config-state.json, not in user-config.json.
- First boot after upgrading migrates config.
config.jsmoves the bookkeeping keys_lastAgentTune,_lastEvolvedand_positionsAtEvolutionout ofuser-config.jsonintoconfig-state.json(next to it) and writesuser-config.json.pre-profiles.bak, a byte-identical copy of your old file. The backup is created once and never overwritten. Your effective settings are unchanged (the profile resolves tocustom). - Rollback: stop the bot, restore the backup (
cp user-config.json.pre-profiles.bak user-config.json), then check out the previous version. Without the restore, the old version sees no evolution bookkeeping and may re-tune thresholds on the next close. learning.autoTuneis now OFF by default. Screening thresholds no longer self-evolve; the bot only logs what it would change. Turn it on in Settings (it asks for a risk confirm) if you want the old behavior.- Bid-ask flip defaults to
shadow(simulate and log only). Switching it toliveneeds the Telegram risk-confirm card. Each flip pass is bounded bybidAskFlip.timeoutSec(default 30 s). - Recommended first boot: dry run. Start with
DRY_RUN=true(ordryRun: true), watch a few management and screening cycles and the logs, then go live from the Settings console.
.env: secrets and endpoints onlyuser-config.json: runtime settings (hand/Telegram-edited; the bot no longer writes counters here)settings-schema.js: the settings table (types, ranges, tiers, profiles)config-state.json: bot-written bookkeeping (gitignored)user-config.example.json: reference config shapeautoresearch.json: prompt experiment statelessons.json: performance-derived lessons
Typical .env (see .env.example):
WALLET_PRIVATE_KEY=your_base58_key
RPC_URL=https://...helius-rpc.com
TELEGRAM_BOT_TOKEN=123456:ABC...
# plus, only if you use them:
# OPENROUTER_API_KEY / DEEPSEEK_API_KEY / MINIMAX_API_KEY, GMGN_API_KEY, HELIUS_API_KEY, JUPITER_API_KEY, LPAGENT_API_KEYNotes:
LPAGENT_API_KEYaccepts a comma-separated list, but use a single key. LPAgent's Terms of Service Β§7 prohibits creating multiple accounts to circumvent restrictions, so rotating keys from several accounts to get around the rate limit risks termination. If you need more throughput, upgrade the plan (Basic 5, Premium 10, Enterprise 20 requests/min) and setLPAGENT_RPMto match. The two-sided spot deploy gate needs a Premium or Enterprise key (/pools/{id}/top-lpers); without one, two-sided spot stays blocked.llmProvider(user-config.json) can beclaude,codex,openrouter,deepseek, orminimaxOPENROUTER_API_KEYis only needed when provider isopenrouterDEEPSEEK_API_KEYis only needed when provider isdeepseekMINIMAX_API_KEYis only needed when provider isminimaxclaudeandcodexproviders use OAuth login, no API key neededTELEGRAM_CHAT_IDshould be your own chat id. If it (andtelegramChatIdin user-config) is empty, the first private chat to message the bot is registered once, with a loud warning in the logTELEGRAM_ALLOWLIST(optional) is a comma-separated list of Telegram user ids allowed to act inside the owner chat; needed when the owner chat is a groupdryRunlives inuser-config.json(flip it from the Telegram console); keep ittrueuntil you validate behavior
If you run Meridian in a normal terminal, it starts:
- the web server
- cron jobs
- the PnL watcher
- the REPL
On startup it may preload candidate pools. In that interactive flow:
- entering
1,2,3, and so on deploys into the numbered startup candidate gostarts autonomous mode without an immediate manual deploy
If Meridian is started without an interactive terminal, it still:
- starts the web server
- starts cron jobs
- starts the PnL watcher
It simply skips the REPL prompt.
On launch Meridian typically:
- loads env and config
- deduplicates lessons
- restores any active autoresearch experiment
- starts the web server
- starts Telegram polling if configured
- starts the PnL watcher
- starts management and screening cron cycles
Meridian is intentionally conservative about overlapping work.
- management and screening cycles do not run on top of each other
- management can defer if another action is already in progress
- screening can skip if max positions are reached
- screening can skip if wallet balance is too low
- management can skip when there are no open positions
- the PnL watcher runs independently from the main cycles
If the bot looks idle, check logs for skipped, deferred, or balance / max-position guards before assuming it is broken.
Common commands:
1,2,3... deploy into numbered startup candidategostart autonomous mode without manual deploy/statusshow wallet and open positions/candidatesshow current top candidates/briefingshow performance briefing/thresholdsshow screening thresholds/learnrun pool study/evolveevolve screening thresholds/stopgraceful shutdown<wallet_address>inspect another wallet- free-form text chat with the general agent
The web UI exposes the same chat surface and a command palette.
Send /menu (or /start, or tap the persistent π Menu button) for the button menu: Status Β· Positions Β· Candidates Β· Wallet Β· Settings Β· Bot controls Β· Trading settings. Views edit the same message in place and have Refresh / Back buttons. Every text command from the REPL still works (/status, /candidates, /settings, /usdc, /autoresearch β¦, /help, β¦).
Deploying a candidate (the π Deploy button, or replying with its number after /candidates) opens a short picker in the same message: choose Bid-Ask, Spot or Hybrid (all single-sided SOL; β marks your configured strategy), then a range: Auto (with rangeDepthMode: "ohlcv", the default, the pool's candle-based depth with its basis, e.g. "Auto 62% (1mΒ·full life 5.5h drawdown 48% Γ1.3, gmgn)"; otherwise the pool's volatility), 25%, 50% or 80%. deploy_position enforces a 35% minimum range and, in ohlcv mode, widens any range shallower than the candle depth, so narrower presets are labelled with the depth they widen to; a preset that would fall under the 20-bin minimum at the pool's bin step is hidden. The confirmation card then shows the strategy, range, approximate bin count and amount. Two-sided spot is not offered. Under activeStrategy: "evil_panda" the picker is skipped (fixed Evil Panda spot plan), and in USDC mode only Bid-Ask is offered.
Token lookup: paste a token mint (or send /token <mint> / /lookup <mint>) and the bot answers with a card instead of sending it to the chat agent. The card lists up to 5 of the token's SOL-quoted Meteora DLMM pools (by fee/active-TVL, then TVL) in the Candidates format, the GMGN token signals (mcap, holders, age, 1h/24h change, smart money / KOL, supertrend / RSI), and β
/β lines for your current screening filters (bin step, TVL, volatility, fee/aTVL, mcap, holders, organic, token age when configured). Each pool has a Deploy button into the same strategy/range picker and confirmation card; a manual entry may proceed when filters fail, and the β lines are repeated on the confirmation card. Every deploy_position safety check still applies, including the bin-step range, max positions and the balance checks. Blacklisted tokens and non-SOL pools get no Deploy button. If GMGN is slow or rate-limited, the card still renders with "GMGN data unavailable"; the lookup is capped at about 15s.
Trading settings (βοΈ Trading settings, from the main menu or Settings): preset buttons for take profit (3β20%), stop loss (β5 to β20% or Off), trailing TP (on/off, trigger 3/5/8/10%, drop 2/3/4/6%), out-of-range wait (5β60 min), deploy size (0.5/1.1/1.5/2 SOL or Customβ¦, 0.1β10 SOL; sets deployAmountSol and maxDeployAmount together for a fixed size, and raises minSolToOpen to at least size + gas reserve), max positions (1β4) and the PnL watcher interval (15/30/60s). β
marks the current value. A tap saves to user-config.json and applies to the running bot at once (a new PnL watcher interval reschedules the watcher). Changes that raise risk (stop loss Off or wider, a bigger deploy size, more positions, trailing TP off) need a second tap on a 60s single-use confirm; the rest apply in one tap. Every change shows old β new and is logged. Stop loss Off is stored as 0. The agent's update_config can still change these keys either way.
All settings (βοΈ Settings β π§Ύ All settings) is the complete editor; Trading settings and π‘ Entry filters stay as the quick preset screens. It lists every key config.js reads from user-config.json and gmgn-config.json (derived from config.js itself, so new keys appear automatically), grouped as Mode / Capital & sizing / Exits / Strategy / Screening (Meteora) / Screening (GMGN) / Entry filters / Schedule / LLM / Learning / USDC mode / Other, with current values. Secrets (wallet key, RPC URL, API keys, chat ids, anything in .env) and the locked keys are never listed. Tap a key: booleans get On/Off, enums get their valid values, numbers and text take your next message (validated for type, range and allowed values; /cancel aborts; the prompt expires after 60s). dryRun needs a second tap in both directions and warns clearly before going LIVE (if .env sets DRY_RUN, .env wins again after a restart). Risk-raising edits (bigger size, more positions, wider or off stop loss, trailing TP off, an entry guard off, a lower gas reserve, USDC mode on/off) also need the second tap. Changes save to the right file and apply live where the running config supports it; llmProvider and webPort apply after a restart.
Wallet (π°, read-only) shows the true total: SOL, USDC and every token worth more than $0.10 in the wallet (Helius prices), plus each open DLMM position's value (SOL side, token side, unclaimed fees; LP Agent / Meteora values, which exclude fees, so fees are added) with a subtotal, and the combined total in SOL and USD at the SOL price shown. A position whose value is unknown is listed as "value unknown" and left out of the total. Refresh forces a fresh position scan.
Bot controls: pause/resume scheduled screening (persisted in state.json; management and the PnL watcher keep running), run a screening cycle now, autoresearch status/list/approve/reject, and the last ERROR/WARN log lines (redacted).
Access and confirmation rules:
- only the owner chat (
telegramChatIdinuser-config.json, elseTELEGRAM_CHAT_ID) can control the bot; the sender must be that chat or listed inTELEGRAM_ALLOWLIST. Other chats are logged and ignored, never answered - with no owner configured, the first private chat is registered once (logged loudly) and persisted to
user-config.json - anything that moves funds (close, deploy, number-reply deploy,
auto, run screening now) shows a confirmation card first. Confirm carries a single-use nonce that expires after 60s; it executes through the sameexecuteToolpath the agent uses, so DRY_RUN and every safety check still apply
Telegram alerts only fire when something happened: deploys, closes, stop-loss / take-profit auto-closes, partial deploys, out-of-range (once per 6h per pair), gas low and cycle errors, with Positions / Close (confirmation) buttons, plus the daily briefing. A management or screening report is sent only when a close rule fired or funds moved; routine "all HOLD" or "nothing deployed" cycles are silent and show under Status instead.
Default dashboard URL:
http://localhost:3737
The dashboard is websocket-driven and live-updated. On connect it receives an initial payload, then refreshes wallet, positions, timers, candidates, and activity as the bot runs.
Main areas:
- status bar with timers and wallet state
- chat panel
- dashboard tab for wallet, LP overview, and positions
- candidates tab
- activity tab
If the dashboard does not load in normal bot mode, make sure the frontend was built:
cd web
npm install
npm run build
cd ..npm run dev:web is for frontend development only. It is not required for normal bot operation.
Everything in user-config.json is optional, but these are the main knobs.
| Field | Purpose |
|---|---|
dryRun |
Simulate or trade live |
llmProvider |
claude, codex, openrouter, deepseek, or minimax |
managementModel |
manager model |
screeningModel |
screener model |
generalModel |
chat model |
autoresearchModel |
prompt-optimizer model |
screeningSource picks where candidates come from: "meteora" (default, Meteora pool discovery with the thresholds below), "gmgn" (GMGN token pipeline, filters in gmgn-config.json), or "both". With "both", the two run in parallel and the bot keeps going on one if the other fails. Results are deduped to one pool per token (higher fee/active-TVL, then higher TVL). GMGN-only pools must also pass minBinStep/maxBinStep, minTvl/maxTvl and maxVolatility, and pools found by both sources rank first. Invalid values fall back to "meteora".
| Field | Meaning |
|---|---|
screeningSource |
meteora, gmgn, or both |
minFeeActiveTvlRatio |
minimum fee / active TVL |
minTvl, maxTvl |
TVL bounds |
minVolume |
minimum pool volume |
minOrganic |
minimum organic score |
minHolders |
minimum holder count |
minMcap, maxMcap |
market-cap bounds |
minBinStep, maxBinStep |
bin-step bounds |
maxVolatility |
volatility ceiling |
maxPriceChangePct |
price-change ceiling |
timeframe |
screening timeframe |
category |
discovery bucket |
minTokenFeesSol |
anti-bundle / anti-spam floor |
athTopThresholdPct |
ATH proximity threshold |
| Field | Meaning |
|---|---|
deployAmountSol |
minimum SOL per deploy (floor; a smaller computed size skips the deploy) |
maxPositions |
maximum concurrent positions |
minSolToOpen |
minimum wallet balance to allow new deploy |
gasReserve |
reserve left for gas |
positionSizePct |
dynamic sizing fraction |
positionSizeBase |
total (default) or wallet: what positionSizePct is a fraction of (see Deploy sizing below) |
outOfRangeWaitMinutes |
OOR hold time before action |
emergencyPriceDropPct |
emergency-drop cutoff |
stopLossPct |
stop-loss threshold |
takeProfitFeePct |
take-profit threshold |
trailingTakeProfit |
enable trailing exits |
trailingTriggerPct |
trailing activation point |
trailingDropPct |
trailing giveback threshold |
priorityFeeLevel |
transaction fee preset |
pnlUnit |
sol or usd display |
With no explicit amount, each SOL deploy is sized as:
base = total SOL (positionSizeBase "total": free wallet SOL + SOL value of every open DLMM position, unclaimed fees included once)
| free wallet SOL (positionSizeBase "wallet")
size = min(maxDeployAmount, positionSizePct Γ (base β gasReserve))
amount = min(size, free wallet SOL β gasReserve) # never more than the SOL actually free to deploy
amount < deployAmountSol β skip, e.g. "size 0.62 below floor 1.1" # the floor is never forced
On the total basis, if any open position's value is unknown or the positions/price lookup fails, the size falls back to the free-wallet basis (smaller, never inflated) and the fallback is logged. minSolToOpen still gates screening on free SOL as before. The Telegram deploy card and the agent's deploy instruction show the basis, e.g. 1.13 SOL = 45% of (2.61 SOL total β 0.1 reserve). USDC mode sizes in USD (deployAmountUsd) and is unaffected.
| Field | Meaning |
|---|---|
managementIntervalMin |
management cycle cadence |
screeningIntervalMin |
screening cycle cadence |
pnlWatcherIntervalSec |
watcher cadence |
maxSteps |
max ReAct steps per agent turn |
| Field | Meaning |
|---|---|
temperature |
generation temperature |
maxTokens |
max tokens per turn |
managementFallbackModel |
optional manager fallback |
screeningFallbackModel |
optional screener fallback |
generalFallbackModel |
optional general fallback |
| Field | Meaning |
|---|---|
autoresearch |
enable prompt experiments |
autoresearchModel |
model used for prompt edits |
autoresearchReasoningEffort |
Codex reasoning effort for autoresearch |
autoresearchMinClosesPerArm |
closes needed in each A/B arm before a verdict (default 100) |
autoresearchMinEffectPct |
minimum size-weighted mean PnL gain, in pp, to pass (default 1.5) |
autoresearchMaxExperimentDays |
time cap; at the cap the result is inconclusive and the candidate is discarded (default 14) |
autoresearchAutoKeep |
keep a passing candidate without operator approval (default false) |
autoresearchMaxDiffPct |
reject candidates that change more than this % of the section's lines (default 30) |
autoresearchCooldownCloses |
cooldown between experiments |
| Field | Meaning |
|---|---|
darwinianWeights |
enable adaptive signal weights |
darwinianWindowDays |
lookback window |
darwinianBoostFactor |
positive adjustment multiplier |
darwinianDecayFactor |
negative adjustment multiplier |
darwinianWeightFloor |
lower bound |
darwinianWeightCeiling |
upper bound |
darwinianMinSamples |
minimum sample count |
Every closed position can produce a structured lesson in lessons.json. Lessons are deduplicated and injected into prompts by role.
Prompt budget shape:
- pinned lessons up to 10
- role-matched lessons up to 15
- recent lessons fill the remaining budget up to 35 total
pool-memory.json records every deploy and close per pool address (PnL, range efficiency, strategy, close reason, win rate) plus mid-position snapshots. Management and screening prompts get it as POOL CONTEXT; the agent can read it with get_pool_memory and annotate a pool with add_pool_note.
The earlier Nuggets memory layer was removed. If an older install still has data/nuggets/, it is ignored and can be deleted.
Meridian can evolve screening thresholds from real performance and lesson history. This is opt-in: set autoTune: true. When off (the default) the same analysis runs but only logs proposed changes (evolve_proposed); nothing in user-config.json moves. Applied changes persist back into user-config.json.
- A close is a win above +1% PnL and a loss below β1%. Anything in between is break-even and doesn't count toward wins or losses. The same classifier (
learning-data.js) drives lessons, pool-memory win rates and signal weights. - Every evolution rule needs at least
MIN_RULE_SAMPLES(5) of the closes (or, for lesson-based rules, lessons) it relies on before it moves a setting. A rule that compares winners with losers needs 5 on each side it uses. - Records flagged
exclude_from_learningorcorrupt, records markedcorrected, and the known-bad list inlearning-data.jsare never learned from. - On the exit side, evolution tunes only the trailing take profit, one 0.5 step per run:
trailingTriggerPct(1.5β15) andtrailingDropPct(1β8). It learns only from closes with a known peak that ran under the current value.takeProfitFeePctandstopLossPctare left to you.
Autoresearch is prompt optimization driven by real closes, inspired by karpathy/autoresearch. Live PnL is a noisy, non-stationary metric, so it runs as a concurrent A/B test and a human approves any change.
It:
- waits until there is enough close history, off the close path (never blocks a close)
- attributes recent losses to a prompt section that is active for the current strategy
- asks the generator for one small change, steered by the human-edited
autoresearch-program.md - rejects candidates that touch HARD RULE / HARD SKIP / MUST / NEVER lines or change too much
- alternates screener runs between the control text and the candidate, tagging each deploy with its arm
- after
autoresearchMinClosesPerArmcloses per arm, passes only if the bootstrap 95% CI of the size-weighted mean PnL difference is above 0 and the gain is at leastautoresearchMinEffectPct - turns a pass into a pending proposal for the operator (
/autoresearch approve | reject), unlessautoresearchAutoKeepis on
Section targets: screener_criteria, plus range_selection when the strategy isn't evil_panda. manager_logic can't be split into concurrent arms, so it isn't targeted.
Kept overrides persist in autoresearch.json and are applied only while autoresearch is enabled. /autoresearch list | show | revert | restore manage them from Telegram or the REPL. The legacy overrides from the pre-A/B loop are quarantined there.
Meridian uses:
- Meteora DLMM SDK for on-chain positions and transactions
- Meteora discovery / PnL endpoints for pool and position data
- Helius RPC for chain access
- Jupiter for token data and swaps
- LP Agent for overview, history, and top-LPer study
meridian/
index.js
agent.js
prompt.js
config.js
state.js
lessons.js
autoresearch.js
pool-memory.js
unified-memory.js
server.js
telegram.js # Bot API transport + owner-only access control
telegram-ui.js # menus, views, confirmations, alerts
llm-provider.js
tools/
web/
Set either:
WALLET_PRIVATE_KEYin.env- or
walletKeyinuser-config.json
Verify:
codex login
codex exec --model gpt-5.4 "Reply with OK"If the direct CLI call fails, Meridian will fail too.
Build the frontend:
cd web
npm install
npm run build
cd ..Check for:
dryRun- max-position guard
- low SOL balance
- deferred management or screening
- no open positions for management
Only the owner chat is accepted (telegramChatId in user-config.json, else TELEGRAM_CHAT_ID). Change telegramChatId to rebind ownership. In a group owner chat, add the members' user ids to TELEGRAM_ALLOWLIST; ignored updates are logged as TELEGRAM_WARN.
This software is provided as-is, without warranty. Running an autonomous trading agent carries real financial risk and can lose funds. Start in dry run, validate behavior, and size capital conservatively.
strategy: "hybrid" (Telegram: Settings > Liquidity shape, or the picker's Hybrid button) deploys ONE single-sided SOL position (bins_above = 0) that holds a Bid-Ask layer and a Spot layer over the same bin range. The Bid-Ask layer keeps buying dips with the heaviest weight at the bottom; the Spot layer adds a flat base that earns fees across the whole range.
- Mix:
hybridBidAskPct(default 60, range 1-100, Tuning tier). The Spot share is derived (100 - hybridBidAskPct, shown ashybridSpotPct); it is not a separate setting, so the two can never be set to a bad sum. 100 means no Spot layer. - Lamport split: Bid-Ask gets
floor(total x pct); Spot gets the remainder, so the legs always sum exactly to the deposit. - Transactions: up to 69 bins is ONE atomic transaction (
initialize_position+ Bid-Ask deposit + Spot deposit), so there is no half-deployed state. Wider ranges use the existing chunked path: create the extended position, send the Bid-Ask layer's chunk transactions and confirm them on-chain, then the Spot layer's (they are not merged: one merged chunk needs ~1.37M of the 1.4M compute units). Every existing deploy safety layer applies unchanged (wallet lock, crash checkpoint, pre-submit price gate, deploy-time screening recheck, deposit slippage, no-new-bin-array rule, priority fees). - Tracking: the position is recorded with
strategy: "hybrid",strategy_type: "Hybrid",strategy_mix {bid_ask_pct, spot_pct}andlegs {bid_ask_sol, spot_sol}. Hybrid is single-sided SOL below price, so the TWAP spike guard, upside-OOR lessons and the Bid-Ask flip treat it like Bid-Ask. - Existing configs are unaffected:
strategy: "bid_ask"still deploys pure Bid-Ask. node scripts/hybrid-sim.jsbuilds the hybrid transactions for real pools and runs mainnetsimulateTransactiononly (never signs or sends).