Skip to content

Solana DeFi Trading Platform

A multi-strategy trading bot for Solana: DEX arbitrage, liquidations, keepers and LST spreads. It ran on mainnet with real capital, lost money, and taught me more in that hour than weeks of simulation had.

paused2026TypeScript · Node.js · Solana · SQLite · PM2
Dry run, sixteen minutes in: four strategies armed, the curve flat because nothing traded
Dry run, sixteen minutes in: four strategies armed, the curve flat because nothing traded

Ran on mainnet with real capital, then was switched off4 strategy families in one process, sharing one wallet300 tests green, and none of them caught the bug that matteredJito bundles on the liquidation path, plain submission everywhere else

The problem

Solana has more short-lived inefficiencies than any single strategy can absorb: price gaps between venues, undercollateralised loans waiting to be liquidated, protocol jobs nobody has run yet, and liquid-staking tokens drifting from their redemption value.

Chasing them one strategy at a time means four separate bots, four wallets, and four ways to lose an account. The part I wanted to work out was not the trade. It was running four strategies from one process without letting any of them take the others down.

What it does

One process runs four strategy families against Raydium, Orca, Meteora, Drift and MarginFi. It decides how much capital each one may touch and enforces a ceiling per strategy as well as one across the account. Liquidations go out as Jito bundles, since those are the ones worth sandwiching.

A local dashboard shows what each strategy is doing and the ceilings it runs under. Telegram carries the alerts. A single kill switch stops submission everywhere at once.

State
Ran on mainnet, now switched off
Storage
SQLite, so there is no database to lose
MEV
Jito bundles on the liquidation path only
Safety
One global kill switch, one ceiling per strategy

Architecture

  • Jupiter APIPolled quotes, 20s cycle

    Scanners

  • Solana RPCAccounts and subscriptions

    Scanners

  • ScannersOne per strategy

    Decision

  • DecisionGates, then sizing

    Execution

  • ExecutionSubmit, then measure
third partymy hardwaretwo origins, one decision
01

Scanners

Four of them, one per strategy. Two read polled HTTP quotes, two hold live subscriptions to on-chain accounts, and they all emit the same shape of candidate.

02

Strategy orchestrator

Runs the four strategy loops in one process and arbitrates when two of them want the same capital at the same moment.

03

Capital allocator

A ceiling per strategy and one for the account as a whole. A strategy cannot borrow room from another one, even when it is winning.

04

Risk manager

Global and per-strategy limits, with a kill switch that halts submission everywhere at once rather than unwinding strategy by strategy.

Built with

Runtime

TypeScript
Strict mode, because a wrong number here spends money
Node.js
One process holds all four strategy loops

Chain

@solana/web3.js
Transaction building, and reading balances back after a fill
Jupiter API
Quotes across venues, and the swap route actually executed
Jito bundles
Liquidation submissions only, where front running actually pays
Drift and MarginFi SDKs
Order books and health factors for the keeper and liquidation strategies

State

SQLite
Every trade, so a post mortem does not depend on log retention

Operations

PM2
Restarts the process without restarting the machine
HTTP dashboard
Local page showing per-strategy state and the active ceilings
Telegram
Alerts when a limit is hit or the kill switch fires
SIGNALGATESSIZESUBMITMEASURED

Every stage runs on hardware I own

The four strategies

S1

DEX arbitrage

ran on mainnet

Quotes the same pair on three venues, looks for a gap wider than the fees on both sides, and takes it in two swaps. This is the only one that ever executed against real money, and the trade it executed lost.

  • Jupiter quotes6 pairs on 3 venues

    Spread check

  • Spread checkNet of fees, under 10 percent

    Risk gates

  • Risk gates3x gas, ceilings, cooldown

    Two swaps

  • Two swapsUsdc to token to usdc
third partymy hardwarethe 10 percent cap exists because of one real loss
S2

Liquidation

half connected

Watches borrowers on two lending venues and repays a loan that has fallen under water, taking a bonus on the seized collateral. It is the only strategy that submits through Jito, because a liquidation is worth front running. The MarginFi half stopped decoding when the protocol changed on chain.

  • MarginFiBank and margin accounts

    Health factor

  • DriftLive account subscription

    Health factor

  • Health factorAssets over liabilities

    Bonus check

  • Bonus check2.5 and 1.5 percent

    Jito bundle

  • Jito bundleRepay, seize collateral
third partymy hardwarethe dashed leg is the one that no longer decodes
S3

Keeper

inert by construction

Meant to fill other people's orders on a perpetuals venue for a small fee paid by the protocol. It subscribes to the book, polls it every two seconds, and discards every candidate, because the reward it estimates sits below the floor it enforces on itself.

  • Drift order bookLive subscription, 2s poll

    Fillable orders

  • Fillable ordersCrossed, not yet filled

    Reward floor

  • Reward floor5 cents against a 50 cent floor

    Nothing emitted

  • Nothing emittedEvery candidate discarded
third partymy hardwaregreen on the dashboard, incapable in practice
S4

LST arbitrage

never fired

Computes what a liquid staking token is really worth by dividing the SOL held in its stake pool by the supply, compares that to what the market pays for it, and swaps when the two drift apart. The fair side of that comparison is arithmetic read from chain, not an oracle, so it cannot be quoted wrong the way an illiquid pool can.

  • Stake pool accountsSol staked over supply

    Fair value gap

  • Jupiter quoteWhat the market pays

    Fair value gap

  • Fair value gapArithmetic, not an oracle

    Divergence gate

  • Divergence gateAbove 0.3 percent

    Swap

  • SwapjitoSOL and bSOL
third partymy hardwaretwo tokens passed stake pool validation

Same grid, same reading direction, one path per strategy

Under the hood

01Profit is measured at the wallet, not estimated

The first version recorded what a trade was expected to earn, computed before submission, as the trade's result. That is a comfortable way to build a strategy that always looks profitable. Now the executor reads the wallet balance from the confirmed transaction and takes the difference, so a recorded profit is something that actually arrived. The estimate still exists, but it is only used to decide whether to try, never to say what happened.

src/executor/keeperExecutor.ts
// Calculate real profit from wallet SOL balance change
const preBalance = txDetails?.meta?.preBalances?.[0] ?? 0;
const postBalance = txDetails?.meta?.postBalances?.[0] ?? 0;
const balanceChangeSol = (postBalance - preBalance) / 1e9;
actualProfitUsd = balanceChangeSol * solPrice;

What replaced a line that read: estimated reward, minus gas.

02The kill switch is global, not per strategy

The first version stopped strategies one by one. That is fine while you are watching. It is useless at 3am, when the reason you want to stop is that you do not yet know which strategy is misbehaving. It now halts submission everywhere at once and leaves the diagnosis for later.

03A strategy that pays nothing is worse than one that loses

The keeper strategy settled other users' unrealised profit on a perpetuals venue, on the assumption that the protocol paid a share of what was settled. It does not. That instruction moves accounting between accounts and pays the caller nothing at all. The scan is now switched off behind a constant rather than deleted, because the reasoning is worth more in the file than in a commit message.

src/scanner/driftOrderScanner.ts
if (!SETTLE_PNL_DISABLED) {
  this.scanUnsettledPnl().catch(() => {});
  this.settlePollTimer = setInterval(() => {
    this.scanUnsettledPnl().catch(() => {});
  }, this.settlePollIntervalMs);
} else {
  log.info('Settle PnL scanning disabled (no external keeper reward)');
}

Gallery

Per-strategy state, and the ceilings each one runs under before it may submit
Per-strategy state, and the ceilings each one runs under before it may submit

Try it

Nothing to try. It is switched off.

It works, in the narrow sense that it did what it was told against real venues with real money. What it does not do is win, because these strategies are a race decided by proximity to the validators. I built it, ran it, learned what I wanted from it, and stopped it before it could teach me the same lesson at a larger size.

The architecture section is the interesting part, and it is all on this page.

What broke

Would change

The database recorded profit that never existed

The keeper strategy wrote its own pre-trade estimate into the field meant to hold the result. Seven transactions went out on mainnet, each one real, each one costing gas, and each one was recorded as a large gain. The ledger showed a winning strategy while the wallet showed nothing arriving. An estimate and a result must never share a field, and the result has to be read back from the chain.

Would change

A spread that good is a bug in your data, not a gift

The scanner found a gap of roughly sixty-six percent on an illiquid token between two venues and took it. Both legs executed on-chain exactly as designed, which is the uncomfortable part: the code was correct and the signal was not. The quoted price was not executable at size, and slippage ate the trade. The detector now rejects anything above ten percent outright and simulates before committing.

Would change

I wrote a nonce manager that nothing ever calls

Four strategies signing in parallel from one account will race for blockhashes, so I built a single authority to hand them out, with a mutex and a queue. It is still there, and not one executor imports it. The same is true of the transaction queue next to it. I had described both as load bearing until I went looking for their callers, which is the uncomfortable lesson: a component you designed carefully is the one you are least likely to check is wired in.

Where it stops

The keeper strategy cannot emit a task, by construction

It estimates a filler reward as a flat five cents per fill, then refuses anything paying under fifty cents. Every candidate is discarded before it becomes work, and the other half of the strategy, settling other users' profit, is switched off because it pays nothing at all. The process still subscribes to the order book and polls it every two seconds. A strategy can be alive in the logs, green on the dashboard, and structurally incapable of doing anything.

Would do again

Going to mainnet early, with an amount I could afford to lose

In simulation the strategy was profitable with a perfect win rate, replaying the same route dozens of times. Real execution disagreed on the three things a dry run cannot model: slippage, competition, and the fact that an arbitrage can only be taken once. An hour against real money found four critical bugs and settled the viability question. I would spend it again rather than scale a simulation I trusted.

Where it stops

Where it stops: latency, and it is a hosting bill

The bots that win these races sit in the same datacenter as the validators and measure the whole loop in milliseconds. This one polls from a home connection on a free RPC tier, three orders of magnitude slower. There is no version of the code that closes that gap, and renting the gap away costs more per month than the bot would plausibly make at my size. So it is off, and that is the honest answer.

Next projectHybrid