An autonomous, self-hosted crypto trading bot with an LLM for a brain. You give it a small stake on Kraken and an API key for the LLM of your choice; it manages the money on its own schedule, keeps a full written diary of every thought, and pushes you a daily digest. Your only control is the halt button β by design.
And it puts the LLMs against each other. Gemini, Claude, GPT, Grok and DeepSeek can each run as a shadow arm: the same prompt, the same market, the same instant, the same fees β trading in simulation beside the real bot, and beside dumb-momentum, never-sell and coin-flip rule arms. The leaderboard names the model behind every row, so you can see which brain is actually any good β and whether any of them beat a coin flip. That's the experiment. One bot proves nothing; a bot with controls might.
β οΈ This is an experiment, not a product. An LLM has no proven trading edge. Magpie is built for people who want to watch an AI manage a toy stake with real rails around it β not for money you can't afford to lose. Expect anything from slow bleed to pleasant surprise. The house always takes 0.4% a trade.
π New to this, or on Windows? Jump to the step-by-step Windows guide β install to uninstall, no coding needed.
Dashboard a few months in: the leaderboard ranks the brain against its control arms β a dumb EMA rule, a DCA, a coin flip and doing nothing β while "Was it right?" grades every call at its horizon and asks whether the model's confidence means anything. Simulated preview data; a fresh install starts humbler.
Four sleeves, each an independent sub-portfolio with its own books, its own mandate, and its own decision cadence:
| Sleeve | Horizon | Decides | Funded by |
|---|---|---|---|
| swing | ~1β3 days | every 6 h | β of the stake |
| fortnight | 1β2 weeks | daily | β of the stake |
| quarter | weeksβ3 months | Mondays | β of the stake |
| vault | a year+ | 1st of month | profits only |
Each decision cycle, the engine builds a context pack β that sleeve's holdings, market data with computed indicators (EMA 20/50/200, RSI, multi-horizon returns), its own recent decision history, and a fee reminder β and asks its LLM for a strict-JSON decision: buy / sell / hold, pair, fraction, confidence, reasoning.
The validation layer is the real boundary: only whitelisted pairs, spot only, long only, exchange minimums enforced, balances reconciled β and any malformed, out-of-universe or errored answer resolves to HOLD. The model's words never touch the exchange; only validated orders do.
The vault starts empty. Whenever an active sleeve's value exceeds its high-water mark, half of the realised profit (EUR actually banked, not paper gains) is skimmed into the vault, and the mark ratchets up. The vault accumulates long-term positions out of house winnings only β losing sleeves are never refilled from it.
It learns from itself. Once a month it re-reads its own diary β every decision, what it reasoned at the time, and what actually happened β and writes itself a lessons note that is injected into all future prompts. The only memory it carries forward is the one it earns.
It trades like a local. Orders go in as post-only limits at the touch (maker fee ~0.25%) with a patient window before falling back to market (~0.40%) β a guaranteed saving on every fill that patience can win. Decisions see daily and 4-hour indicators, the live spread, and the Crypto Fear & Greed index; the slow sleeves think with a stronger model than the fast ones.
It keeps score against a hodler. From its first sight of capital it tracks a phantom buy-and-hold portfolio of the same money (topped up in lockstep) β the dashboard and daily digest always show whether the AI is beating doing nothing, and the monthly self-review is confronted with the number. Closed trades are FIFO-paired into round trips (win rate, average win/loss, hold time), and a nightly reconciliation keeps the virtual sleeve books honest against real exchange balances.
Top-ups: deposit more EUR to the exchange whenever you like. The bot notices the surplus at its next cycle, splits it equally across the three active sleeves, and raises their high-water marks so fresh cash is never mistaken for profit.
Magpie isn't wed to one model. LLM_PROVIDER chooses who makes the call:
| Provider | LLM_PROVIDER |
API key from |
|---|---|---|
| Gemini (Google) β default | gemini |
aistudio.google.com |
| OpenAI (ChatGPT) | openai |
platform.openai.com |
| Anthropic (Claude) | anthropic |
console.anthropic.com |
| Perplexity | perplexity |
perplexity.ai β API |
| Grok (xAI) | grok |
x.ai |
| DeepSeek | deepseek |
platform.deepseek.com |
| GitHub Models (Copilot) | github |
github.com personal access token |
| OpenRouter β one key, any model above and more | openrouter |
openrouter.ai |
Each provider uses its own key β set the one for the brain you pick. The prompt
and the strict-JSON safety layer are identical whichever model answers, so a
model that formats badly simply resolves to HOLD. LLM_MODEL / LLM_MODEL_DEEP
override the per-provider default models (the deep one runs the slow sleeves and
the monthly self-review; the fast one runs the rest). Switch live from the
settings page β dropdown, key, Test active brain, save; no restart.
A paid ChatGPT / Perplexity / Copilot subscription is not an API key β each needs a developer key from the provider's platform, billed per token.
β οΈ Gemini's free tier works, but don't depend on it. It is heavily rate-limited and heavily used, and it will hand you429 Too Many Requestsat busy times β sometimes for a run of retries in a row. Magpie fails safe (a refused call is a HOLD, never a bad trade) and the retry timer usually gets the decision through minutes later, so nothing breaks. But a sleeve can silently sit out a slot it meant to act on, and you'll seeFAILED β out of quotain the diary. It's fine for trying Magpie out. If you want the brain to answer when it's asked, enable billing on the Gemini project or pointLLM_PROVIDERat a paid one.
The ledger records what the exchange actually settled β the filled amount, the average price it really got, and the fee it really charged β not what the bot assumed would happen. That distinction matters more than it sounds: Kraken gives you the full amount you bought and charges the fee on top, so a bot that models the fee instead of reading it disagrees with reality on every single trade, and the difference gets quietly laundered into the nightly reconcile as anonymous "drift". Drift absorption is meant to be the exception, not the accounting.
Paper and shadow modes mirror the same fee convention, so a simulated arm is playing the same game as the live bot rather than a slightly easier one.
Every stamp on the dashboard is shown on your clock (TIMEZONE) β the same clock the
decision slots run on.
Backups. The database is the audit trail β every prompt, decision, order and fill. POST /api/backup writes a crash-consistent copy (SQLite's VACUUM INTO, safe against a live
writer β unlike cp or a filesystem snapshot of a WAL-mode file, which usually restores)
into BACKUP_DIR, keeping the last BACKUP_KEEP (default 7). Schedule it alongside the other
timers. If you already snapshot the host, point it at the copies, not the live file.
-
The API key cannot withdraw. Create it with query + trade permissions only. Worst-case compromise of the box = bad trades, never stolen funds. (The repo's verification snippet in
docs/probes this.) -
It can never deposit β there is no payment integration. The stake you fund is the ceiling.
-
Spot only, long only, whitelisted pairs only β no leverage, no derivatives, no liquidations.
-
Kill switch:
POST /api/halt(or the big red button on the dashboard) stops all ordering untilPOST /api/resume. -
Total auditability: every prompt sent to the model and every raw response is stored. The dashboard diary shows what it saw, what it thought, and what it did β for every cent that moves.
-
Stop-losses, if you want them (
STOP_LOSS_ENABLED, off by default): a protective sell rests at the exchange, so it still works when the bot does not β see Stop-losses.
There is deliberately no position cap and no circuit breaker β no rule that halts the strategy after a bad run. The operator takes the risk knowingly and holds the halt button. Add your own guardrails if that's not your temperament.
The single most dangerous failure in a bot like this is not a bad trade. It is a bot that cannot act, rendered as a bot that chose not to.
Every "do nothing" outcome here looks the same from the outside β a flat line, a HOLD, an empty row β and doing nothing is a perfectly respectable decision. That is exactly what makes it such good camouflage. Every one of these has actually happened in this repo:
| What was really going on | What it looked like |
|---|---|
| The deep model was retired; quarter and vault 404'd on every call | Two sleeves calmly holding |
| Half the portfolio was cash nobody had drawn the model's attention to | A brain patiently staying in cash |
| A shadow arm had no API key and was never asked anything | A strategy with the conviction to hold |
| An arm's provider ran out of credit and died | Two flat, disciplined equity curves |
| An arm was dropped from the config; its record was deleted from the table | An arm that never existed |
Not one of those announces itself. Each renders as restraint β the most plausible-looking output an unthinking system can produce β and a bot whose whole claim is "an LLM is managing this money" silently stops being true.
So the rule, everywhere in this codebase:
If the bot could not act, it must say so β loudly, and in the place you are already looking. It may never fall back on a silence that reads as a decision.
In practice: a sleeve that never got an answer is a β FAILED row in the diary, not a HOLD. A dead arm is marked β not answering on the leaderboard, not left flat. A retired arm keeps its row and states why it stopped. Idle cash is named in the prompt, so holding it has to be argued for rather than defaulted into. An arm that cannot speak is dropped before it can pose as one that did.
A HOLD in this bot should always mean "I thought about it and chose to wait." If it can ever mean "nobody was home", the diary is fiction and the leaderboard is measuring nothing.
You need: Docker, a dedicated Kraken account that holds only the money you want Magpie to manage, a trade-only API key (see above), and an API key for one supported LLM β Gemini is the default and its free tier is enough to get started (the bot makes only a handful of calls a day) β though it is rate-limited enough that it will refuse some of them, so don't lean on it. See The brain for the alternatives.
β οΈ Use an empty Kraken account. On its first live cycle Magpie treats the entire balance of the account as its starting stake and splits it across the sleeves, and its nightly reconciliation pulls any coin sitting on the account into its own books. So fund a fresh account with nothing else in it β just the stake you're giving Magpie. Don't point it at an account holding coins or cash you don't want it trading.
cp .env.example .env # fill in your Kraken + LLM keys; leave TRADING_ENABLED=false
docker compose up -d --build
The bot starts in paper mode: identical code path, live market data,
simulated fills against a pretend stake. Watch the diary at http://<host>:8000
until you trust it, then set TRADING_ENABLED=true and recreate the container.
At its first live cycle it treats your entire exchange EUR balance as the opening
top-up and splits it across the sleeves.
Schedule the heartbeat (systemd timer or cron):
0 0,6,12,18 * * * curl -s -X POST http://localhost:8000/api/cycle
5 18 * * * curl -s -X POST http://localhost:8000/api/digest
45 5 * * * curl -s -X POST http://localhost:8000/api/reconcile
30 5 1 * * curl -s -X POST http://localhost:8000/api/review
The cycle endpoint is safe to call at any hour β sleeve cadences are gated internally (fortnight only acts on the 06:00 call, quarter on Monday's, the vault on the 1st of the month).
Magpie runs anywhere Docker does. On Windows β or if you're not very technical β follow the complete step-by-step Windows guide: install, run, schedule, go live, update and uninstall, all click-by-click with no coding.
GET /healthβ liveness, mode, halt state, last decisionGET /api/stateβ full portfolio, sleeve breakdown, decision diary, skimsPOST /api/cycleβ run a decision tickPOST /api/digestβ push the daily summaryPOST /api/reviewβ run the monthly self-review (writes the lessons note)POST /api/reconcileβ absorb drift between the sleeve books and exchange realityGET /settingsβ a page to enter/change keys and Home Assistant details (secrets masked; test buttons for each integration). Web-entered settings persist and override the env.POST /api/halt/POST /api/resumeβ the only human controlsGET /api/armsβ the leaderboard: the bot, every shadow arm, and the phantom hodlerGET /api/stopsβ resting stop-losses (real orders at the exchange, in live mode)POST /api/stops/cancelβ clear every resting stop (a halt deliberately does not)POST /api/topup?amount=β paper mode only; live deposits are auto-detected
Base currency. Magpie trades against and values everything in one currency β EUR by default, or USD, GBP, etc. It's a one-time choice at initial setup (Settings β Base currency): the moment you set it β and automatically once the bot has traded β it locks permanently, because switching it on a funded account would leave your holdings and exchange balance in the wrong currency. Choose it before funding.
Timezone. Set TIMEZONE (an IANA name like America/New_York, default
Europe/Dublin) β or pick it on the settings page β so the daily 06:00, Monday
and 1st-of-month decision slots run on your local clock. It's safe to change
anytime; align it with the schedule you set below.
Everything else is env vars β see .env.example. Notables:
LLM_PROVIDER (which brain; default gemini) and its matching API key,
LLM_MODEL / LLM_MODEL_DEEP (optional model overrides), PAIRS (the base
tradeable universe, default BTC/EUR + ETH/EUR), and SKIM_FRACTION (profit share
skimmed to the vault, default 0.5).
The deep brain. Rare, expensive decisions β the quarter sleeve, the vault and the
monthly self-review β use a stronger model (LLM_MODEL_DEEP, or GEMINI_MODEL_DEEP).
DEEP_PROVIDER / DEEP_MODEL let those few calls live on a different provider than the
frequent cheap ones: useful when your main key has no quota for the big model, which is a
common free-tier limit. Empty = use LLM_PROVIDER, as before.
Pin an alias, not a preview. Model ids get retired β
gemini-2.5-pronow returns "no longer available to new users", andgemini-3-pro-previewis already gone. Prefer a tracking alias likegemini-pro-latest. When a model does vanish the sleeve fails safe to HOLD, and the dashboard now says so plainly instead of promising a retry that cannot help.
Settings beat env. Anything saved on the
/settingspage persists in the database ascfg_<KEY>and overrides the environment. Editing.envfor a key you have already set in the UI does nothing β change it in the UI (or clear the row).
The features below are all off unless you turn them on:
SHADOW_ARMS (rival strategies traded in simulation β shadow arms),
STOP_LOSS_ENABLED (stop-losses),
CONTEXT_FUNDING / CONTEXT_DEPTH / NEWS_RSS_URL (what the brain sees),
and DYNAMIC_UNIVERSE / DYNAMIC_TOP_N / DYNAMIC_SELL_FLOOR_N
(dynamic universe).
Magpie can push every event β trades, top-ups, the daily digest, error alerts and
the monthly self-review β to as many channels as you like. Set the ones you want
(in .env or on the settings page); each fires only when its keys are present, and
every alert fans out to all configured channels at once:
| Channel | Config |
|---|---|
| Home Assistant push | HA_URL + HA_TOKEN + HA_NOTIFY_SERVICE |
| Pushover | PUSHOVER_TOKEN + PUSHOVER_USER |
| Pushbullet | PUSHBULLET_TOKEN |
| Discord (rich embed) | DISCORD_WEBHOOK_URL |
| Telegram | TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID |
| ntfy | NTFY_TOPIC (+ NTFY_SERVER, default ntfy.sh) |
HA_NOTIFY_CLICK_URL (e.g. your dashboard URL) becomes the tap-to-open link on
every channel that supports it, and the settings page's Test button sends a
test to all configured channels at once. Configure nothing and Magpie just runs
quietly β one channel failing never blocks the others.
With DYNAMIC_UNIVERSE=true, the tradeable set is your base pairs plus the
top-DYNAMIC_TOP_N (default 5) altcoins by market cap that trade against EUR
on Kraken β stablecoins and wrapped/staked tokens excluded. It refreshes on a
weekly timer (POST /api/universe/refresh) and pushes a heads-up when the set
changes.
It never strands a position, and never churns fees on a ranking reshuffle: a
held coin that slips out of the top-DYNAMIC_TOP_N stays sellable at the bot's
own discretion, and only once it falls past DYNAMIC_SELL_FLOOR_N (default 10)
is it force-sold at the weekly refresh β the band between the two is a grace
zone the model manages itself. Base pairs are never auto-sold, and sub-β¬1 dust
is left in place. GET /api/universe shows the current set.
Pin your own coins. Beyond the base pairs and the auto-tracked alts, add any
coin that trades against EUR on Kraken from the settings page's Custom coins
card (or MANUAL_PAIRS / POST /api/pairs/add {symbol}). Each is validated
against Kraken before it's saved, is always tradeable regardless of the rankings,
and β because you chose it deliberately β is exempt from the sell floor.
One bot cannot tell you whether it is any good. If it ends the year up 6%, was that skill, luck, or simply a rising market? Shadow arms are rival strategies that trade in simulation alongside the real bot, on exactly the same market data, in the same sleeves, with the same fees β so any gap between the equity curves is the difference in the decisions, and nothing else.
SHADOW_ARMS=ema:rule:ema20,dca:rule:dca,coinflip:rule:random
Rule arms need no API key and cost nothing to run:
| decider | what it does |
|---|---|
ema20 |
Holds what is above its 20-day EMA, sells what falls below. Dumb momentum β the bar the LLM must clear to justify itself. |
dca |
Buys a fixed slice of its remaining cash into the first base pair every slot. Never sells. The "do nothing clever" arm. |
random |
A coin flip. The null hypothesis. If the brain cannot beat this over months, that is the most useful thing this project will ever tell you. |
LLM arms run a rival brain on the identical prompt β same mandate, same market data, same instant, same validation. Only the model differs, which is what makes it a fair bake-off: whatever the equity curves do, the prompt was not the variable.
SHADOW_ARMS=claude:llm:openrouter@anthropic/claude-sonnet-5,deepseek:llm:openrouter@deepseek/deepseek-chat
The spec is provider@model (model optional β the provider's default is used). One
OpenRouter key reaches Claude, GPT, Grok, DeepSeek and the rest, so a
bake-off needs one account rather than five. Each llm arm costs roughly one API call per due sleeve
per cycle β pennies a day, but not nothing. A rival brain that errors, or babbles, holds its own
arm and nobody else's.
List every LLM in the running, the brain included. Magpie drops whichever arm is the current brain, so a model is never both the contestant and its own rival:
SHADOW_ARMS=ema:rule:ema20,dca:rule:dca,coinflip:rule:random,gemini:llm:gemini,claude:llm:openrouter@anthropic/claude-sonnet-5,deepseek:llm:openrouter@deepseek/deepseek-chat
LLM_PROVIDER |
arms that actually run |
|---|---|
gemini |
ema, dca, coinflip, claude, deepseek |
anthropic |
ema, dca, coinflip, gemini, deepseek |
deepseek |
ema, dca, coinflip, gemini, claude |
Change the brain and the model it displaces keeps being measured β which is exactly when you most
want to know whether its replacement is actually any better. The match is on the model, not the
provider: an arm on gemini-pro against a gemini-2.5-flash brain is a genuine rival and still
runs, while an arm reaching the brain's own model through another gateway
(openrouter@anthropic/claude-sonnet-5 against a direct anthropic brain) is the same weights on
the same prompt, and is dropped.
The leaderboard names the model behind every row, so you can always see who is actually deciding.
An arm whose key you don't have simply doesn't run. Copy the line above wholesale, set only the keys you actually own, and the rest are dropped before they start β no errors, and no phantom row on the leaderboard. That last part matters: a seeded but keyless arm would sit there with a flat curve and 0 trades, which reads as a strategy that chose to hold. It chose nothing β it was never able to answer. A row in that table is a claim about skill, and an arm that cannot speak has made none.
Rule arms need no API key and cost nothing to run. An arm is simply another value in the
mode column, so it gets its own books, its own decision diary and its own equity history,
and portfolio.execute() only ever reaches the exchange when the mode is live β a shadow
arm cannot place a real order. Arms start from the real bot's stake and receive the same
top-ups, so the comparison stays honest; a broken arm is logged and stepped over and can
never disturb the real bot, or wake you at 3am.
Caveat worth knowing: shadow fills are simulated at the touch price and assume the maker limit always fills, so the arms are mildly optimistic against a live bot that pays real slippage.
Empty (the default) = no arms, and not one line of the live path changes.
STOP_LOSS_ENABLED=true
When the bot buys, it also rests a protective sell at the exchange, a distance below
its entry that it chooses itself and the config clamps (STOP_LOSS_MIN_PCT..STOP_LOSS_MAX_PCT,
default 8%). The point of the stop living at Kraken rather than in this process is that it
still works when the bot does not β in the six hours between cycles, through an LLM
outage, a crashed container, a reboot, or while you are asleep.
This is not a position cap and not a circuit breaker on the strategy. It is a floor under one position.
Three things worth knowing:
- A halt does not cancel resting stops. They are protective; cancelling them would leave
positions naked while trading is paused. Clear them deliberately with
POST /api/stops/cancel. - A sell cancels that sleeve's stops first, and refuses to sell if it cannot. The sleeves are virtual books over one real Kraken account, and a resting stop does not know about sleeves β it just sells coins. An orphaned stop would sell a different sleeve's position.
- A fired stop is booked as a sale, with a row in the decision diary and the ledger β it is never quietly absorbed as "drift" by the nightly reconcile. The sale is claimed first, precisely so it cannot be.
Alongside candles, EMA/RSI/returns, the live spread and the Fear & Greed index, the bot is given:
| source | what it is | default |
|---|---|---|
| Perp funding + open interest | What longs are paying shorts to hold their position on Kraken's public futures book. Persistently positive funding means the crowd is leveraged long β a classic warning. Kraken quotes funding absolute, so it is normalised against the mark price. | on |
| Order-book depth | Resting bid vs ask size within 1% of mid. A short-horizon signal, most useful to the swing sleeve. | on |
| News headlines | Any RSS feed, via NEWS_RSS_URL. |
off |
The funding and open-interest numbers are read-only sentiment: the bot looks at the perpetuals market without ever trading it, and stays spot-only and long-only. No API key, no new risk.
News is off by default and deliberately so. Crypto headlines are mostly noise and shilling, and an LLM is suggestible β this is the one context source that can plausibly make decisions worse. If you switch it on, A/B it with a shadow arm rather than assuming it helped.
Every source is optional garnish: a dead feed leaves its key out of the prompt and never fails a cycle.
Every buy and sell the bot makes is a falsifiable claim about direction, and it states a confidence when it makes it. Scoring marks that homework: each decision is graded at its own sleeve's horizon (swing 3 days, fortnight 10, quarter 90) against the price that actually happened, using candles already in the database β no new feeds, nothing to configure, and it runs on the nightly reconcile timer.
Holds are not graded: a hold makes no claim about direction.
The dashboard reports the hit rate bucketed by stated confidence, because the interesting question is not only "does it beat 50%" but does its confidence mean anything β if the 0.9 calls land no better than the 0.6 calls, the number is decoration. The measured record is also injected into the monthly self-review, which until now was the model marking its own homework from memory.
Shadow arms are graded too, so you can see whether the brain's calls are better than a coin flip's even when its returns are not.
Set DASHBOARD_PASSWORD (env or the settings page's Security card) to require a
password for the dashboard, portfolio view and controls. It's a single-password
cookie login; the health check and the timer-triggered action endpoints stay
open (they expose no data). Leave it blank to run open on a trusted LAN or behind
your own reverse-proxy auth.
Two-factor (TOTP). Once a password is set, turn on 2FA from the Security card: scan the QR into Google Authenticator / Authy / 1Password, confirm a code, and every login then needs the 6-digit code as well as the password. It's standard TOTP, verified at login before the session cookie is issued.
Enabling 2FA hands you 10 single-use backup codes (shown once, stored hashed).
Any one works in the login's code field in place of your authenticator and is then
spent β the way back in if you lose your phone. Regenerate the set anytime from the
Security card (needs a current code). If you lose the authenticator and the codes,
the last resort clears it from the container:
docker exec magpie sqlite3 /data/magpie.db "DELETE FROM settings WHERE key IN ('totp_enabled','totp_secret','totp_backup_codes')".
MIT Β© 2026 Colm Finn.
Built by Colm Finn. The magpie trades alone; the consequences are its keeper's.