Written for people who already run liquidity: what gets picked and why, how the range is chosen, where the line between principal and income is drawn, and how to pull all of it as data instead of screenshots.
$AMM is run by one process holding one key. Every sixty seconds it executes the same five steps, and each step is a signed transaction from the wallet the coin was launched from, which pump.fun recorded as its coin_creator and which owns every position the agent has ever opened.
claim creator fees from the pump.fun vault -> capital harvest swap fees from every open position -> income buyback 100% of income, swapped to $AMM <- income deploy whatever capital is left, into one pool <- capital record cycle + position rows, for this API
The ordering is the argument, not a convenience. Buybacks run before deployment, so the balance left in the wallet afterwards is capital by construction. There is no moment in the cycle where income and capital are the same pile of SOL waiting to be split by a ratio someone chose.
/api/v1/ledger totals, which are kept separate for exactly that reason.Nobody maintains a pool list. Each cycle the agent rebuilds one. Birdeye supplies two mint lists rather than one: what is trending, which skews to new tokens with sharp turnover and short lives, and what is doing the most 24h volume, which surfaces the established pairs that quietly do size all day. A book built from either alone is lopsided. DexScreener then says which Meteora DLMM pools those mints actually trade in and how much depth sits behind that volume. Three sweep queries catch the rest.
Everything then has to survive four filters:
| Filter | Threshold | Why |
|---|---|---|
| venue | Meteora DLMM only | Fees must be claimable separately from principal. See Principal vs income. |
| pairing | one side is SOL | SOL is the only asset the agent holds; a single-sided deposit can only be made in an asset the pool takes. |
| liquidity | ≥ $25,000 | Below this the agent's own deposit moves the pool it is trying to earn from. |
| volume 24h | ≥ $20,000 | Fees are a share of volume. No volume, no reason to be there. |
| age | ≥ 48h | A pool younger than this has no history worth scoring. |
What survives is scored on one number: 24h volume divided by pool liquidity. Call it turnover. Fees are a share of volume and are split across the depth in range, so volume-per-dollar-of-depth is the closest honest proxy for what a position would earn that can be computed from public data alone.
The shortlist is cached in Redis for five minutes and read by both the agent and this site, so what you see on /pools is the exact candidate set the next cycle draws from, not a reconstruction of it.
The pick is weighted random over the top twenty, not the argmax. Always taking the leader would put the whole book in one market and chase whatever spiked in the last hour; weighting by turnover keeps the choice informed without making it deterministic.
shortlist = candidates.slice(0, 20) ticket = random() * sum(shortlist.turnover) pick = first candidate where running total >= ticket
Before that, the agent decides whether it is opening or compounding. Below six open positions it leans towards opening (35% of cycles, and always when it holds nothing at all); at the limit every cycle compounds into a pool it already holds. Opening a position it cannot afford to keep harvesting is how a book turns into a pile of rent.
Liquidity is deposited as SOL only. That is not a style preference. The agent's income is SOL, so depositing a pair would mean buying the other token first, which is a price view on every token it touches, taken automatically, sixty times an hour.
A one-sided deposit can only occupy bins on the side of the active price that the asset can be sold into. If SOL is the pool's quote token, the position sits at and below the active bin; if SOL is the base token, at and above it. Placing it on the wrong side is not a worse position, it is a rejected instruction.
As price walks into the range, the pool sells the agent's SOL for the paired token bin by bin. That is ordinary LP behaviour and the reason a position can come back as a mix of both assets rather than the SOL that went in.
A DLMM pool is a row of discrete price bins. Each bin holds liquidity at one price, trades inside a bin have zero slippage, and exactly one bin is active at any moment. A position is a contiguous range of them.
| Choice | Value | Reasoning |
|---|---|---|
| range width | 34 bins | Wide enough to keep earning through normal movement, narrow enough that the capital is not spread across prices that never trade. |
| shape | Spot | Even weight across the range. A curve concentrates on a price the agent has no view on. |
| side | one side of active | Forced by the single-sided deposit. |
| per pool | one position | Every extra position is another account to read, another claim transaction, and another rent deposit that does not come back while it is open. |
Compounding into an existing position reuses its original range rather than re-centring on the current active bin. Re-centring every minute would mean tearing the position down and rebuilding it, paying rent and fees each time, to chase a price that moves again before the next cycle.
Everything the machine claims rests on one distinction: money the coin generated (capital) versus money the liquidity earned (income). Capital becomes positions. Income buys the coin. Neither ever does the other job.
This is only checkable because of a specific property of DLMM: swap fees accrue outside the position rather than compounding into it. So the fees a position has earned are a number that can be read to the lamport and claimed on its own.
The second half of the guarantee is procedural. Harvest amounts are read off the positions before the claim transaction, never diffed from the wallet balance afterwards. The wallet holds SOL for three reasons at once, and a balance diff cannot tell them apart, which would quietly let deployment capital be spent as though it were income.
Every cycle sweeps every pool the agent holds a position in, whatever discovery says about those pools today. Liquidity nobody claims from earns nothing on paper and still pays its rent.
positions = getPositionsByUserAndLbPair(wallet) claimable = positions where feeX > 0 or feeY > 0 amounts = sum(feeXExcludeTransferFee), sum(feeYExcludeTransferFee) // read first txs = claimAllSwapFee(wallet, claimable) // then claim
Fees come back in the pool's own two tokens, so a harvest is a set of legs rather than one number. The SOL leg is what the ledger reports as totalHarvestedLamports; other legs are recorded per cycle in harvestLegs and swapped in the buyback step.
A pool that fails to answer costs only itself. The loop catches per pool, so an RPC error on one market does not cost the others their cycle.
Each harvested leg is swapped to $AMM through Jupiter, which routes across every venue on Solana at once. The agent never picks a market; it asks for the best execution available that second.
A leg below the buyback floor is not dropped, it waits. The floor is checked against the wallet balance rather than the cycle's own amount, so dust accumulates across cycles and goes out with a later harvest of the same mint instead of being stranded.
Bought $AMM stays in the wallet. Nothing is burned and nothing is redistributed; the supply is simply held off the market by the address that bought it, and every buy is on chain under one auditable key.
Defaults below. All lamport figures; 1 SOL = 1,000,000,000 lamports.
| Parameter | Default | Effect |
|---|---|---|
| TICK_INTERVAL_MS | 60,000 | Cycle period. An overrunning tick skips the next rather than queueing, so two cycles never spend the same balance. |
| MIN_CLAIM_LAMPORTS | 0.003 SOL | Below this a creator-fee claim costs more in fees than it collects. |
| MIN_DEPLOY_LAMPORTS | 0.02 SOL | Floor for opening or topping up a position. |
| MIN_BUYBACK_LAMPORTS | 0.002 SOL | Floor for a buyback swap. Smaller harvests wait. |
| POSITION_RENT_LAMPORTS | 0.06 SOL | Held back for position and bin-array rent. Refunded on close, but spent at open. |
| GAS_RESERVE_LAMPORTS | 0.015 SOL | Never spent. Keeps the next hour of ticks able to pay for themselves. |
| SLIPPAGE_BPS | 300 | Bound on swaps and deposits. |
| POOLS | unset | Pins a pool list, disabling discovery. Held positions are still worked. |
The interesting part of an autonomous agent is what it does when something is wrong. It never guesses, and it never spends to find out.
| Condition | Behaviour |
|---|---|
| Mint still pending | Refuses to start. Nothing to claim, and guessing a mint means buying the wrong coin with real money. |
| Wallet is not the coin creator | Refuses to start. pump.fun fixes the creator at mint and it cannot be reassigned, so this is permanent, not transient. |
| Wallet underfunded | Refuses to start, rather than producing sixty identical failures an hour. |
| Discovery returns nothing | Cycle records waiting with a reason and deploys nothing. |
| One pool unreachable | Caught per pool. Other pools still harvest and the cycle completes. |
| Swap or deposit reverts | Cycle records failed with the error. Funds stay in the wallet and the next cycle picks them up. |
| Supabase unavailable | The chain is the record; the cycle still executes and only the bookkeeping is lost. |
| Balance below the floor | Records waiting, which is the agent behaving correctly and is served through the API as such. |
No key, no rate limit beyond a ten-second edge cache, CORS open to any origin. Every response is { data, meta }; errors replace data with error and keep the same envelope, so a client parses one shape. All amounts are lamports as decimal strings, never numbers: a lamport total does not survive a double.
curl https://amm-bot.org/api/v1/state | jq '.data.ledger'
Everything at once: mint, ledger, open positions, recent cycles and the current pool shortlist. Each source is allowed to fail independently, and meta.sources says which of them answered, so an empty list is distinguishable from an unavailable one.
Cumulative totals. Claimed, harvested, bought back and deployed are four separate figures because the only interesting question about this machine is whether the buybacks came out of what the liquidity earned.
Every position the agent has opened, newest deposit first. Amounts are cumulative flows, not a valuation: what went in, and what came back as fees.
The live shortlist the agent draws from, best turnover first, with held marking the pools it is already in. Same cached list the agent reads, so this is the set the next cycle chooses from.
One row per tick, newest first. Waiting rows are served rather than filtered out; hiding them would make the machine look busier than it is.
Ledger, the cumulative state.
| Field | Type | Meaning |
|---|---|---|
| mint | string | null | The coin, read from Redis at request time. Null before launch. |
| wallet | string | null | The agent's address. Creator, signer, and owner of every position. |
| cycleCount | number | Ticks executed, including waiting ones. |
| workedCount | number | Ticks that harvested, bought back or deployed. |
| totalClaimedLamports | string | Creator fees claimed. Capital in. |
| totalHarvestedLamports | string | SOL leg of fees the positions earned. Income. |
| totalBuybackLamports | string | SOL spent buying the coin. Should track income, not capital. |
| totalTokenBought | string | Coin units acquired, in base units. |
| totalDeployedLamports | string | Capital placed into positions. |
| lastTickAt / lastWorkAt | string | null | ISO timestamps. A gap between them means recent cycles found nothing to do. |
| lastError | string | null | Reason the most recent failed cycle failed. Cleared by the next success. |
Position.
| Field | Type | Meaning |
|---|---|---|
| positionId | string | The Meteora position account. |
| poolId | string | The DLMM pair it lives in. |
| pair | string | Readable pair, from discovery where available. |
| lowerBinId / upperBinId | number | The bin range the liquidity occupies. |
| depositedLamports | string | Cumulative SOL deployed into it. |
| harvestedLamports | string | Cumulative SOL fees taken back out of it. |
| deposits / harvests | number | Times each has happened. |
| status | string | open | closed. |
Cycle.
| Field | Type | Meaning |
|---|---|---|
| status | string | worked | waiting | failed. |
| reason | string | null | Why a cycle waited or failed, in plain text. |
| claimedLamports | string | Creator fees claimed this tick. |
| harvestedLamports | string | SOL leg harvested this tick. |
| harvestLegs | array | Every leg as claimed: { mint, amount }, including non-SOL tokens. |
| buybackLamports | string | SOL spent on buybacks this tick. |
| tokenBought | string | Coin units acquired this tick. |
| buybackSignatures | string[] | Transaction signatures, verifiable on chain. |
| deployedLamports | string | Capital deployed this tick. |
| positionsOpened / positionsToppedUp | number | What the deploy step did. |
| durationMs | number | Wall clock for the tick. |
Pool candidate.
| Field | Type | Meaning |
|---|---|---|
| pool | string | The DLMM pair address. |
| pair / baseSymbol | string | What it trades. |
| liquidityUsd | number | Depth, from DexScreener. |
| volume24hUsd | number | 24h volume. |
| velocity | number | Turnover: volume over depth. The score it is ranked by. |
| priceChange24h | number | null | Percent. |
| held | boolean | Whether the agent currently holds a position here. |