most people meet Grok Bot through someone else's pnl screenshot, open it, point it at a chart, and lose the first $50 to a token that was dead before they bought it. the bot wasn't the problem. nobody told it what dead looks like.
this is the setup I'd build again tonight if I had to start over. one bot, one folder, ten small files. it doesn't hunt for 100x. its main job is killing tokens, and the few that survive still need Grok to read X before a single dollar moves.
steps 01 to 07 are the first evening: the bot, the key, the limits, the exits. 08 to 12 are the rug filter, 14 checks, every one of them written down as code. 13 and 14 put it on autopilot, on paper first.
if you read my eight-agent desk post, this is the floor that desk stands on.
https://x.com/0xboan/status/2092226208234647626
how to use this: open your Grok Bot, paste this whole article, and say "build this desk in /workspace/desk, paper mode, ask me before anything touches a wallet." it writes every file itself, on its own computer.
the split you hold onto: code kills, Grok reads X, you sign. nobody on the desk does someone else's job.
01. get four things before you start
four things. each one says what it is, what you do tonight, and why the desk needs it.
1. Grok Bot
what it is. the app where you hire named bots. each one works on a cloud computer with a browser, files and a terminal, and keeps working with your laptop shut.
what you do. download it at x.ai/bot, sign in, choose Create your own.
why. the whole desk lives on that computer. files, routines, alerts, all in one place you can open from your phone.
2. an xAI API key
what it is. paid access to Grok from code, with X search built in.
what you do. console.x.ai, API Keys, create one. don't paste it anywhere yet, step 03 says where it goes.
why. a routine can't sit and think about forty tokens in a chat. a script that makes one call per token can, and it logs every answer in the same format.
3. Rugcheck and DexScreener
what it is. two free public APIs. Rugcheck reads the contract: authorities, LP, holders, insiders, the dev wallet. DexScreener reads the pool: depth, age, buys against sells.
what you do. nothing. no signup, no key for reads.
why. facts come before opinions. an open mint authority isn't a judgment call, it's a fact, and facts kill first.
4. a burner wallet, but not tonight
what it is. a fresh Solana wallet that has never held anything else.
what you do. nothing yet. week one runs on paper.
why. the desk never holds a key. when you go live, it tells you what to buy and you sign it yourself.
02. hire one bot, not eight
my desk runs eight bots now. it didn't start that way, and yours shouldn't either. eight bots on day one is eight things to debug at once. one bot with one job shows you what the other seven will need later.
choose Create your own and give it this, word for word:
1Name: RUG DESK2Job: Solana memecoin desk, paper first3Description: Build and run the desk in /workspace/desk from the article I paste.4Every token goes through check.py, no exceptions, no shortcuts.5You never buy, sell, sign or move funds. You never edit config.py unless I ask6in this chat. You never lower a filter number to let a token through.7Report every kill with its reason, not your opinion of the token.8When a source is down, say which one and skip. Never guess a missing number.
the last two lines matter more than the first five. a bot that guesses a missing number will eventually guess one that costs you.

03. keep the key out of the chat
everything you type to a bot lands in a transcript. so the key goes into a file, typed by you, on the bot's computer.
open the bot's computer, click Take over, open the terminal and run this. it asks for the key without echoing it, so it never shows up in history either:
1mkdir -p /workspace/desk && cd /workspace/desk2read -rs -p "xai key: " K && printf 'XAI_API_KEY=%s\n' "$K" > .env && unset K3chmod 600 .env4pip install requests
then prove the key works before anything else. one call, so you never confuse a bad key with a bad prompt later:
1curl -s https://api.x.ai/v1/responses \2 -H "Authorization: Bearer $(cut -d= -f2 .env)" \3 -H "Content-Type: application/json" \4 -d '{"model":"grok-4.7","input":"reply with OK"}'56-> ... "type":"output_text","text":"OK" ...
click I'm done and hand the computer back. from here on the bot can run the desk, and it still never sees the key in the chat.

04. see the whole funnel before you build any of it
eight passes. the free ones run first. only what survives the free passes costs a Grok call, and only what Grok calls BUY costs money. this week, not even that.
10. LEAD a CA from anywhere: a call, a post, scan.py -> one mint21. LIMITS book.py: daily loss, open slots, cooldown -> free, no network at all32. FACTS rugcheck.py: 10 hard kills -> open mint, unlocked LP, fat top 10 die here43. POOL market.py: depth, age, sells -> thin pools and honeypots die here54. JUDGE grok.py + X search, one call -> BUY or SKIP, one reason, one risk65. FILL paper: book.py at the live price -> live: you sign it yourself76. WATCH watch.py every 10 minutes -> stop, ladder, trail, time stop87. REPORT report.py at 21:30 -> what died, what paid, what to question
an open mint authority isn't a judgment call. it's a fact, and facts are free. Grok only ever sees tokens that already passed every check a script can do.

05. put every number in one file
save as config.py. every number on the desk lives here, so when you retune, you edit one file and nothing else. these are mine, tuned for a $25 ticket. they are not a recommendation, they are a starting point you will move.
1# config.py2# every number on the desk lives here. when you retune, you edit this file and nothing else.34MODEL = "grok-4.7" # check console.x.ai -> Models if the API rejects it5PAPER = True # week one: fake fills at real prices. flip only after you read the log67TICKET_USD = 25 # flat. never sized off the last win8MAX_OPEN = 4 # 4 x $25 = $100 in the market, max9MAX_ENTRIES_PER_DAY = 610DAILY_LOSS_HALT_USD = 50 # realized loss today hits this -> no new entries until tomorrow11COOLDOWN_HOURS = 24 # no re-entry into a token you just left1213FACTS = { # the rug filter. code only, no model14 "max_risk_score": 40, # rugcheck score_normalised, 0-100, higher = riskier15 "min_lp_locked_pct": 90, # biggest pool: LP locked or burned16 "max_top10_pct": 30, # top 10 holders, pools and lockers excluded17 "max_dev_pct": 5, # supply still sitting in the creator wallet18 "max_insiders": 0, # insider-flagged holders + insider graph hits19 "min_liquidity_usd": 30000,20 "max_ticket_share": 0.01, # your ticket is never more than 1% of the pool21 "min_age_min": 30, # younger than this, every report is still guessing22 "max_age_hours": 72,23}2425MIN_CONFIDENCE = 0.7 # Grok says BUY below this -> treated as SKIP2627EXIT = {28 "stop_pct": -35, # hard stop from entry29 "ladder": [(100, 0.50), (200, 0.25)], # +100% sells half, +200% sells a quarter30 "trail_pct": 30, # the rest: out when price falls 30% from its peak31 "time_stop_hours": 6, # not up 20% after 6 hours -> out, dead money32 "time_stop_min_pct": 20,33 "liq_drop_pct": 40, # pool liquidity down 40% from entry -> out now34}3536LOG = "checks.csv"37POSITIONS = "positions.json"38TRADES = "trades.csv"
1FLAT TICKET. the size never grows because the last three won.2THE BOT READS config.py, IT NEVER WRITES IT. a number moves when you move it.3A FILTER IS LOOSENED IN WRITING. you change a number, you say why in the chat, same day.
06. run the limits before anything else
save as book.py. the cheapest check on the desk is the one that never calls an API. if today already lost $50, the desk doesn't even look at the token. if four positions are open, same. this file is also the desk's memory: two files you can open yourself, positions.json and trades.csv.
1# book.py2# positions, trades, limits. the desk's memory lives in two files you can open yourself.3import csv, json, os, sys, time4from config import (POSITIONS, TRADES, TICKET_USD, MAX_OPEN, MAX_ENTRIES_PER_DAY,5 DAILY_LOSS_HALT_USD, COOLDOWN_HOURS)67def load() -> dict:8 return json.load(open(POSITIONS)) if os.path.exists(POSITIONS) else {}910def save(book: dict):11 json.dump(book, open(POSITIONS, "w"), indent=2)1213def trades() -> list[dict]:14 return list(csv.DictReader(open(TRADES))) if os.path.exists(TRADES) else []1516def record(mint, symbol, action, usd, pnl, reason):17 new = not os.path.exists(TRADES)18 with open(TRADES, "a", newline="") as f:19 w = csv.DictWriter(f, fieldnames=["time", "mint", "symbol", "action", "usd", "pnl", "reason"])20 if new:21 w.writeheader()22 w.writerow({"time": time.strftime("%Y-%m-%d %H:%M"), "mint": mint, "symbol": symbol,23 "action": action, "usd": round(usd, 2), "pnl": round(pnl, 2), "reason": reason})2425def today_pnl() -> float:26 day = time.strftime("%Y-%m-%d")27 return sum(float(t["pnl"]) for t in trades() if t["time"].startswith(day))2829def guard(mint: str) -> str | None:30 """Limits run before any network call. A blocked entry costs nothing."""31 day = time.strftime("%Y-%m-%d")32 book = load()33 if today_pnl() <= -DAILY_LOSS_HALT_USD: return "halted_daily_loss"34 if mint in book: return "already_open"35 if len(book) >= MAX_OPEN: return "max_open"36 opens = [t for t in trades() if t["action"] == "open" and t["time"].startswith(day)]37 if len(opens) >= MAX_ENTRIES_PER_DAY: return "max_entries_today"38 cutoff = time.strftime("%Y-%m-%d %H:%M", time.localtime(time.time() - COOLDOWN_HOURS * 3600))39 if any(t["mint"] == mint and t["action"] == "close" and t["time"] >= cutoff for t in trades()):40 return "cooldown"41 return None4243def open_position(mint, symbol, price, liq_usd, usd=TICKET_USD):44 book = load()45 book[mint] = {"symbol": symbol, "entry": price, "qty0": usd / price, "qty": usd / price,46 "peak": price, "liq0": liq_usd, "opened": time.time(), "steps": 0}47 save(book)48 record(mint, symbol, "open", usd, 0, "entry")4950def sell(mint, frac_of_original, price, reason) -> float:51 """Sells a fraction of the ORIGINAL size (1.0 = everything left). Returns realized pnl."""52 book = load()53 p = book[mint]54 q = min(p["qty"], p["qty0"] * frac_of_original)55 pnl = q * (price - p["entry"])56 p["qty"] -= q57 action = "close" if p["qty"] <= p["qty0"] * 1e-6 else "partial"58 record(mint, p["symbol"], action, q * price, pnl, reason)59 if action == "close":60 del book[mint]61 save(book)62 return pnl6364if __name__ == "__main__":65 # live mode: after you buy in your own wallet -> python book.py add <mint> <usd> <price> <liq_usd>66 # after you sell by hand -> python book.py close <mint> <price>67 cmd = sys.argv[1]68 if cmd == "add":69 mint, usd, price, liq = sys.argv[2], float(sys.argv[3]), float(sys.argv[4]), float(sys.argv[5])70 open_position(mint, mint[:6], price, liq, usd)71 elif cmd == "close":72 print(round(sell(sys.argv[2], 1.0, float(sys.argv[3]), "manual"), 2))
07. write the exit before the entry
most people decide when to sell while staring at a green candle. that's the worst moment to decide anything. on this desk every exit exists before the token is bought, and the bot just checks it every 10 minutes:
- liquidity pulled 40% from entry: out, all of it, no questions
- down 35%: out
- up 100%: half goes, the rest is the market's money
- up 200%: another quarter goes
- after the first take, the rest leaves when price drops 30% from its peak
- six hours without +20%: out, the slot is worth more than the hope
save as watch.py:
1# watch.py2# every open position, every 10 minutes. prints nothing when nothing changed.3import time4from config import EXIT, PAPER5from market import pool6import book78def watch() -> list[str]:9 events = []10 for mint, p in list(book.load().items()):11 try:12 m = pool(mint)13 except Exception as e:14 events.append(f"{p['symbol']}: price feed down ({e}), holding, will retry")15 continue16 if not m or m["price"] <= 0:17 events.append(f"{p['symbol']}: pool gone. treat as rugged, sell what you can")18 book.sell(mint, 1.0, 0.0, "pool_gone")19 continue2021 price = m["price"]22 b = book.load(); s = b[mint]23 s["peak"] = max(s["peak"], price); book.save(b)24 pct = (price / s["entry"] - 1) * 10025 hours = (time.time() - s["opened"]) / 36002627 def out(frac, why):28 pnl = book.sell(mint, frac, price, why)29 verb = "sold" if PAPER else "SELL NOW in your wallet"30 size = "the rest" if frac >= 1 else f"{int(frac * 100)}%"31 events.append(f"{s['symbol']}: {verb} {size} at {pct:+.0f}% ({why}), pnl ${pnl:+.2f}")3233 if m["liq_usd"] < s["liq0"] * (1 - EXIT["liq_drop_pct"] / 100):34 out(1.0, "liquidity_pulled"); continue35 if pct <= EXIT["stop_pct"]:36 out(1.0, "stop"); continue37 ladder = EXIT["ladder"]38 if s["steps"] < len(ladder) and pct >= ladder[s["steps"]][0]:39 out(ladder[s["steps"]][1], f"take_profit_{ladder[s['steps']][0]}")40 b = book.load()41 if mint in b:42 b[mint]["steps"] += 1; book.save(b)43 continue44 if s["steps"] > 0 and price <= s["peak"] * (1 - EXIT["trail_pct"] / 100):45 out(1.0, "trailing_stop"); continue46 if hours >= EXIT["time_stop_hours"] and pct < EXIT["time_stop_min_pct"]:47 out(1.0, "time_stop")48 return events4950if __name__ == "__main__":51 for e in watch():52 print(e)
1LIQUIDITY FIRST. a pulled pool beats every other rule, even on a green position.2THE TRAIL ARMS AFTER THE FIRST TAKE. before that, -35% is the only floor.3DEAD MONEY IS A LOSS TOO. six flat hours free the slot for the next token.4SILENCE IS A STATUS. watch.py prints nothing when nothing changed, so a quiet chat means you don't have to look.

08. learn the 14 ways a token dies
if I had to throw away everything above and keep one thing, it would be this. it also works without any bot: run the same 14 checks by hand on rugcheck.xyz and dexscreener before your next buy and you'll skip most of the tokens you were about to regret.
13 of the checks are code. comparisons, no model, no opinion. the 14th is the only one that needs Grok.
#
check
kills when
why
1
already rugged
Rugcheck marks it rugged
someone already did it
2
mint authority
still open
the dev can print new supply into your bag
3
freeze authority
still open
the dev can freeze your tokens, you can't sell
4
danger flags
any danger-level risk
Rugcheck already saw something you'd have to prove wrong
5
risk score
above 40/100
small warnings add up
6
LP lock
biggest pool under 90% locked or burned
unlocked liquidity is a pull button
7
top 10 holders
over 30%, pools and lockers excluded
ten wallets can end your trade together
8
dev wallet
creator still holds over 5%
the dev is the first seller
9
insiders / bundles
any insider holder or insider graph hit
wallets funded together sell together
10
pool depth
under $30k liquidity
you'd be someone's exit
11
ticket share
your $25 is over 1% of the pool
your own buy moves the price
12
age
under 30 min or over 72h
too fresh for any report, or the move already happened
13
sells exist
20+ buys in the last hour, zero sells
that's the shape of a honeypot
14
X read (Grok)
no posts, copycat CA, bought hype, rug history
the part no API returns

09. let code kill the obvious ones
checks 1 to 9. it fetches one report and keeps only the fields the desk reads. Grok never sees the raw report, a fat payload costs tokens and costs accuracy.
one detail people miss: the pool's own vault and the lockers sit in the top holders list. count them and every token looks like a whale token. the code drops them before it adds up the top 10.
1# rugcheck.py2# one report per token, trimmed to the fields the desk reads. Grok never sees the raw report.3import requests4from config import FACTS56RC = "https://api.rugcheck.xyz/v1/tokens"78def report(mint: str) -> dict:9 r = requests.get(f"{RC}/{mint}/report", timeout=20)10 r.raise_for_status()11 d = r.json()12 token = d.get("token") or {}13 known = d.get("knownAccounts") or {}1415 def is_pool(h): # AMM vaults and lockers sit in top holders, they are not whales16 tag = known.get(h.get("owner")) or known.get(h.get("address")) or {}17 return str(tag.get("type", "")).upper() in ("AMM", "LOCKER")1819 holders = [h for h in (d.get("topHolders") or []) if not is_pool(h)]20 markets = sorted(d.get("markets") or [],21 key=lambda m: ((m.get("lp") or {}).get("baseUSD") or 0)22 + ((m.get("lp") or {}).get("quoteUSD") or 0),23 reverse=True)24 lp = (markets[0].get("lp") or {}) if markets else {}25 supply = token.get("supply") or 026 risks = d.get("risks") or []2728 return {29 "mint": mint,30 "symbol": (d.get("tokenMeta") or {}).get("symbol"),31 "rugged": bool(d.get("rugged")),32 "risk_score": d.get("score_normalised"),33 "danger": [x.get("name") for x in risks if x.get("level") == "danger"],34 "warn": [x.get("name") for x in risks if x.get("level") == "warn"],35 "mint_authority": token.get("mintAuthority", d.get("mintAuthority")),36 "freeze_authority": token.get("freezeAuthority", d.get("freezeAuthority")),37 "lp_locked_pct": lp.get("lpLockedPct", d.get("lpLockedPct")),38 "top10_pct": round(sum(h.get("pct") or 0 for h in holders[:10]), 2) if holders else None,39 "dev_pct": round((d.get("creatorBalance") or 0) / supply * 100, 2) if supply else None,40 "insiders": sum(1 for h in holders if h.get("insider")) + (d.get("graphInsidersDetected") or 0),41 "holders": d.get("totalHolders"),42 }4344def fact_kill(t: dict) -> str | None:45 """Plain comparisons. First failure wins, the reason gets logged."""46 if t["risk_score"] is None: return "no_report"47 if t["rugged"]: return "already_rugged"48 if t["mint_authority"]: return "mint_open"49 if t["freeze_authority"]: return "freeze_open"50 if t["danger"]: return "danger:" + t["danger"][0]51 if t["risk_score"] > FACTS["max_risk_score"]: return "risk_score"52 if t["lp_locked_pct"] is None or t["lp_locked_pct"] < FACTS["min_lp_locked_pct"]:53 return "lp_unlocked"54 if t["top10_pct"] is None or t["top10_pct"] > FACTS["max_top10_pct"]:55 return "top10_heavy"56 if t["dev_pct"] is None or t["dev_pct"] > FACTS["max_dev_pct"]:57 return "dev_holds"58 if t["insiders"] > FACTS["max_insiders"]: return "insiders"59 return None

10. check the pool, not just the contract
checks 10 to 13. a contract can be perfect and the pool still won't let you out. this reads the deepest pool for the token and asks four boring questions: is there enough in it, how big is my ticket next to it, how old is it, and is anyone actually selling.
1# market.py2# the pool itself: depth, age, price, and whether anyone can actually sell.3import time4import requests5from config import FACTS, TICKET_USD67DS = "https://api.dexscreener.com/tokens/v1/solana"89def pool(mint: str) -> dict | None:10 r = requests.get(f"{DS}/{mint}", timeout=15)11 r.raise_for_status()12 data = r.json()13 pairs = data if isinstance(data, list) else (data.get("pairs") or [])14 pairs = [p for p in pairs if (p.get("baseToken") or {}).get("address") == mint]15 if not pairs:16 return None17 p = max(pairs, key=lambda x: (x.get("liquidity") or {}).get("usd") or 0)18 h1 = (p.get("txns") or {}).get("h1") or {}19 born = p.get("pairCreatedAt")20 return {21 "symbol": (p.get("baseToken") or {}).get("symbol"),22 "price": float(p.get("priceUsd") or 0),23 "liq_usd": (p.get("liquidity") or {}).get("usd") or 0,24 "age_min": round((time.time() * 1000 - born) / 60000) if born else None,25 "buys_1h": h1.get("buys", 0),26 "sells_1h": h1.get("sells", 0),27 "vol_1h": (p.get("volume") or {}).get("h1"),28 "url": p.get("url"),29 }3031def market_kill(m: dict | None) -> str | None:32 if not m: return "no_pool"33 if m["price"] <= 0: return "no_price"34 if m["liq_usd"] < FACTS["min_liquidity_usd"]: return "thin_pool"35 if TICKET_USD / m["liq_usd"] > FACTS["max_ticket_share"]: return "ticket_too_big"36 if m["age_min"] is None or m["age_min"] < FACTS["min_age_min"]:37 return "too_fresh"38 if m["age_min"] > FACTS["max_age_hours"] * 60: return "too_old"39 if m["buys_1h"] >= 20 and m["sells_1h"] == 0: return "nobody_can_sell"40 return None
11. let Grok read what code can't
check 14. this is the reason the desk runs on Grok and not on any other model: it can search X while it thinks. a token can pass all 13 checks and still be a copycat with a different contract address, or a launch where forty accounts made last week posted the same sentence in the same minute. no API returns that. X does.
save as grok.py. it's the only file that reads the key and the only place the desk talks to xAI:
1# grok.py2# the only file that holds the key and the only place the desk talks to xAI.3import os, json, datetime, requests4from config import MODEL56URL = "https://api.x.ai/v1/responses"78def _key() -> str:9 k = os.environ.get("XAI_API_KEY")10 if not k and os.path.exists(".env"):11 for line in open(".env"):12 if line.startswith("XAI_API_KEY="):13 k = line.split("=", 1)[1].strip().strip('"')14 if not k:15 raise RuntimeError("no XAI_API_KEY in the environment or in .env")16 return k1718def _text(resp: dict) -> str:19 if isinstance(resp.get("output_text"), str):20 return resp["output_text"]21 parts = []22 for item in resp.get("output") or []:23 if item.get("type") == "message":24 for c in item.get("content") or []:25 if c.get("type") in ("output_text", "text"):26 parts.append(c.get("text") or "")27 return "\n".join(parts)2829def ask(system: str, payload: dict, read_x: bool = False) -> dict:30 """One call, JSON in, JSON out. Anything that isn't JSON raises, and a raise is a SKIP upstream."""31 body = {"model": MODEL,32 "input": [{"role": "system", "content": system},33 {"role": "user", "content": json.dumps(payload)}]}34 if read_x:35 since = (datetime.date.today() - datetime.timedelta(days=3)).isoformat()36 body["tools"] = [{"type": "x_search", "from_date": since}]37 body["max_turns"] = 2 # two rounds of searching, then it has to answer38 r = requests.post(URL, timeout=180, json=body,39 headers={"Authorization": f"Bearer {_key()}"})40 r.raise_for_status()41 text = _text(r.json())42 start, end = text.find("{"), text.rfind("}")43 if start == -1:44 raise ValueError(f"no json in answer: {text[:200]}")45 return json.loads(text[start:end + 1])
save as prompts.py. this is the file you'll actually reread:
1# prompts.py2# the one Grok prompt on the desk. this is the file you will actually reread.34JUDGE_PROMPT = """5You are RUG DESK. A Solana memecoin just passed every hard check in code:6authorities, LP, holders, dev wallet, insiders, pool depth, age.7Do not re-check those numbers. You decide if one flat ticket goes in.8You never size, you never pick exits.910INPUT: {"token": {...rugcheck facts...}, "pool": {...dexscreener facts...}}1112STEP 1. Search X for the ticker and for the contract address, last 72 hours.13STEP 2. Judge only what code can't see:14 - do the warn-level risks stack into something worse together15 - who posted first: the dev, a real account with history, or a wave of fresh accounts16 - organic or bought: same wording, same minute, accounts with no past17 - is the contract address in those posts the same one as in INPUT18 - has this ticker name or this dev been tied to a rug before1920RETURN ONLY JSON:21{"verdict": "BUY" | "SKIP", "confidence": 0.0-1.0,22 "why": "two lines max", "x_read": "one line on what X looks like",23 "main_risk": "one line"}2425Zero posts on X is an answer: nobody knows it exists. SKIP.26Another contract address under the same ticker on X: copycat risk. SKIP.27NEVER answer BUY when a field you needed is null.28NEVER mention price targets. You don't know them.29"""
12. wire it into one command
one contract address in, one verdict out. limits, facts, pool, Grok, in that order, and the first one that says no ends it:
1# check.py2# one contract address in, one verdict out. runs every time a token crosses your desk.3import csv, json, os, sys, time4from config import LOG, MIN_CONFIDENCE, PAPER, TICKET_USD5from rugcheck import report, fact_kill6from market import pool, market_kill7from grok import ask8from prompts import JUDGE_PROMPT9import book1011def log(row: dict):12 new = not os.path.exists(LOG)13 with open(LOG, "a", newline="") as f:14 w = csv.DictWriter(f, fieldnames=["time", "mint", "symbol", "stage", "verdict",15 "confidence", "reason"])16 if new:17 w.writeheader()18 w.writerow({k: row.get(k) for k in w.fieldnames})1920def check(mint: str) -> dict:21 if (g := book.guard(mint)): # limits, free22 return {"stage": "limits", "verdict": "SKIP", "reason": g}2324 try:25 token = report(mint) # rug filter, free26 except Exception as e:27 return {"stage": "facts", "verdict": "SKIP", "reason": f"rugcheck failed: {e}"}28 if (k := fact_kill(token)):29 return {"stage": "facts", "verdict": "SKIP", "reason": k, "symbol": token["symbol"]}3031 try:32 m = pool(mint) # pool, free33 except Exception as e:34 return {"stage": "pool", "verdict": "SKIP", "reason": f"dexscreener failed: {e}"}35 if (k := market_kill(m)):36 return {"stage": "pool", "verdict": "SKIP", "reason": k, "symbol": token["symbol"]}3738 try:39 ans = ask(JUDGE_PROMPT, {"token": token, "pool": m}, read_x=True) # the only paid step40 except Exception as e:41 return {"stage": "grok", "verdict": "SKIP", "reason": f"grok failed: {e}"}4243 verdict, conf = ans.get("verdict"), ans.get("confidence") or 044 if verdict == "BUY" and conf < MIN_CONFIDENCE:45 verdict = "SKIP"46 ans["why"] = f"low confidence {conf}"47 return {**ans, "stage": "grok", "verdict": verdict, "symbol": m["symbol"] or token["symbol"],48 "reason": ans.get("why"), "price": m["price"], "liq_usd": m["liq_usd"], "url": m["url"]}4950if __name__ == "__main__":51 mint = sys.argv[1].strip()52 v = check(mint)53 log({"time": time.strftime("%Y-%m-%d %H:%M"), "mint": mint, **v})54 if v["verdict"] == "BUY":55 if PAPER:56 book.open_position(mint, v["symbol"], v["price"], v["liq_usd"])57 v["action"] = f"PAPER BUY ${TICKET_USD} at {v['price']}"58 else:59 v["action"] = (f"APPROVAL NEEDED: buy ${TICKET_USD} of {v['symbol']} in your own wallet, "60 f"then run: python book.py add {mint} {TICKET_USD} <fill price> {v['liq_usd']}")61 print(json.dumps(v, indent=2))
you don't need a scanner to use it. a CA from a call channel, a post, a friend's message: tell the bot "check <CA>" and it runs one line. what comes back looks like this (example shape, not a real token):
1{2 "verdict": "SKIP",3 "confidence": 0.78,4 "why": "passes every hard check, but the first 30 posts came from accounts created this week, same wording.",5 "x_read": "one real account, a wave of fresh ones, no CA mismatch",6 "main_risk": "bought hype, the buyers leave when the posting stops",7 "stage": "grok",8 "symbol": "EXAMPLE",9 "reason": "passes every hard check, but the first 30 posts came from accounts created this week, same wording."10}

1FACTS BEFORE OPINIONS. Grok never decides whether a mint is open.2A FAILED CALL IS A SKIP. Rugcheck down, DexScreener down, Grok down, bad JSON: SKIP.3NULL IS NOT CLEAN. a field that didn't come back kills the token, it never passes it.4ONE VERDICT PER TOKEN. never re-run until it says BUY. if you disagree, you disagree with config.py.5READ THE KILLS, NOT THE BUYS. the buys feel good. the kill reasons tell you if the filter works.
13. three routines, one sentence each
no workflow builder. you tell the bot each routine once, in a sentence, and it shows you the next run. these are mine, word for word:
1Every 10 minutes, in /workspace/desk run python watch.py.2If it prints nothing, say nothing. If it prints anything, post it here word for word.
1Every 30 minutes, in /workspace/desk run python scan.py.2Post only BUY verdicts: symbol, confidence, x_read.3If Rugcheck or DexScreener is down, say which one once, not every run.
1Every day at 21:30, in /workspace/desk run python report.py and post the output here.2Add one thing that worked today and one number in config.py you would question, with the reason.3Never change that number yourself.
the two files those routines run. save as report.py:
1# report.py2# the evening report. everything in it comes from checks.csv and trades.csv, nothing from memory.3import csv, os, time4from collections import Counter5from config import LOG, PAPER6import book78day = time.strftime("%Y-%m-%d")9checks = [r for r in csv.DictReader(open(LOG))] if os.path.exists(LOG) else []10checks = [r for r in checks if r["time"].startswith(day)]11done = [t for t in book.trades() if t["time"].startswith(day) and t["action"] != "open"]1213by_stage = Counter(r["stage"] for r in checks if r["verdict"] == "SKIP")14buys = [r for r in checks if r["verdict"] == "BUY"]15wins = [t for t in done if float(t["pnl"]) > 0]16reasons = Counter(r["reason"] for r in checks if r["stage"] in ("facts", "pool"))1718print(f"EVENING REPORT {day} {'(PAPER)' if PAPER else '(LIVE)'}")19print(f"checked: {len(checks)}")20print(f"killed by rug filter: {by_stage['facts']} | by pool: {by_stage['pool']} | "21 f"by Grok: {by_stage['grok']} | by limits: {by_stage['limits']}")22print(f"bought: {len(buys)} | open now: {len(book.load())}")23print(f"realized pnl today: ${book.today_pnl():+.2f}")24if done:25 print(f"green exits: {len(wins)}/{len(done)}")26 best = max(done, key=lambda t: float(t["pnl"]))27 worst = min(done, key=lambda t: float(t["pnl"]))28 print(f"best: {best['symbol']} ${float(best['pnl']):+.2f} | worst: {worst['symbol']} ${float(worst['pnl']):+.2f}")29if reasons:30 top, n = reasons.most_common(1)[0]31 print(f"most common kill: {top} ({n}x)")
and scan.py, the optional lead source. it pulls fresh Solana tokens that just paid for a DexScreener profile. a paid profile is not a quality signal, it only means the dev spent money. the filter decides the rest:
1# scan.py2# optional lead source: fresh Solana tokens that just paid for a DexScreener profile.3# a paid profile is not a quality signal, it just means the dev spent money. the filter decides.4import csv, os, subprocess, sys5import requests6from config import LOG78seen = {r["mint"] for r in csv.DictReader(open(LOG))} if os.path.exists(LOG) else set()9r = requests.get("https://api.dexscreener.com/token-profiles/latest/v1", timeout=15)10r.raise_for_status()11fresh = [t["tokenAddress"] for t in r.json()12 if t.get("chainId") == "solana" and t.get("tokenAddress") not in seen]1314for mint in fresh[:5]: # five per run, so one busy hour can't drain the API budget15 subprocess.run([sys.executable, "check.py", mint])
last thing tonight: Settings, General, Auto-review. add two Ask first rules, they stop the action and wait for you no matter what any routine thinks:
1Ask first before any wallet connection, signature, swap or transfer.2Ask first before editing any file named config.py.


14. a week on paper before a single dollar
PAPER = True means the desk does everything for real except the money. real tokens, real prices, real exits, fake fills. after seven days, open checks.csv and read it like a stranger wrote it.
what you're looking for: which check kills the most, whether the BUYs would have been ones you'd buy, and whether any SKIP reason looks wrong. a paper fill happens at the screen price, a live fill never does, so treat the paper result as the ceiling, not the forecast.
when you go live, flip PAPER to False and nothing else changes except the last step. check.py stops buying and posts an approval line instead: buy $25 yourself in your own wallet, then one command puts it in the book so watch.py starts guarding it.
when it breaks
this touches money, so every failure has one answer written down before it happens.
1401 from xAI -> bad key. Take over, rewrite .env, run the curl from step 03 again.2400 / 404 from xAI -> model name changed. fix MODEL in config.py, nothing else.3429 from xAI -> rate limit. that token is a SKIP, the next run moves on. never loop it.4error about x_search -> your MODEL can't search X. pick one that can on console.x.ai -> Models.5Grok answers in prose -> ValueError, logged, SKIP. never turn a paragraph into a trade.6rugcheck 404 or empty -> too fresh to index. SKIP. fresh is not clean.7lp_unlocked on everything -> some pool types have no LP token at all. open one on rugcheck yourself8 before you touch that number, and change it in writing.9no_pool -> not trading on a DEX yet, or not Solana. SKIP.10routines went quiet -> Grok Bot pauses routines after you've been away a long time.11 open Routines and switch them back on.
the bill
everything before the judge is free: the limits, Rugcheck, DexScreener. the only paid step is one Grok call per token that survived all 13 checks, with X search on.
1cost_per_token = input_tokens / 1_000_000 * price_per_M_input2 + output_tokens / 1_000_000 * price_per_M_output3 + posts_fetched / 1_000 * 5 # X search: $5 per 1,000 posts fetched45max_turns = 2 keeps the search to two rounds, so posts_fetched stays small.6every response reports its own x_posts_fetched, so day one tells you the real number.78price: [price from console.x.ai]
the part people get wrong: the bill scales with survivors, not with the scanner. scan.py can feed the desk 240 tokens a day and most of them die for free before Grok ever hears about them.

important part
this is my setup, not your strategy.
every number in config.py is mine, picked for a $25 ticket and my own tolerance for watching something go to zero. most memecoins do go to zero, and most people who trade them lose money. the filter doesn't make a token good. it removes the obvious ways to lose, and it makes you read why every token died.
run a week on paper. read the kills. move the numbers where you disagree, in writing. copy the shape, not the constants. not financial advice.
code kills, Grok reads X, you sign. a script never gets a judgment call, Grok never gets a calculator, and nobody but you ever gets the wallet.
save this before your next buy. follow if you're planning on running the same thing, more of this coming 🫶
my tg - t.me/+8wAXS0nHWgA2MGEy
my github - github.com/0xboan
trade here - fomo.family/r/0xboan





