Grok Bot from zero in one night: full setup + a copy-paste rug filter
文章目录
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.
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:
Name: RUG DESK
Job: Solana memecoin desk, paper first
Description: Build and run the desk in /workspace/desk from the article I paste.
Every token goes through check.py, no exceptions, no shortcuts.
You never buy, sell, sign or move funds. You never edit config.py unless I ask
in this chat. You never lower a filter number to let a token through.
Report every kill with its reason, not your opinion of the token.
When 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:
mkdir -p /workspace/desk && cd /workspace/desk
read -rs -p "xai key: " K && printf 'XAI_API_KEY=%s\n' "$K" > .env && unset K
chmod 600 .env
pip install requests
then prove the key works before anything else. one call, so you never confuse a bad key with a bad prompt later:
curl -s https://api.x.ai/v1/responses \
-H "Authorization: Bearer $(cut -d= -f2 .env)" \
-H "Content-Type: application/json" \
-d '{"model":"grok-4.7","input":"reply with OK"}'
-> ... "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.
0. LEAD a CA from anywhere: a call, a post, scan.py -> one mint
1. LIMITS book.py: daily loss, open slots, cooldown -> free, no network at all
2. FACTS rugcheck.py: 10 hard kills -> open mint, unlocked LP, fat top 10 die here
3. POOL market.py: depth, age, sells -> thin pools and honeypots die here
4. JUDGE grok.py + X search, one call -> BUY or SKIP, one reason, one risk
5. FILL paper: book.py at the live price -> live: you sign it yourself
6. WATCH watch.py every 10 minutes -> stop, ladder, trail, time stop
7. 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.
# config.py
# every number on the desk lives here. when you retune, you edit this file and nothing else.
MODEL = "grok-4.7" # check console.x.ai -> Models if the API rejects it
PAPER = True # week one: fake fills at real prices. flip only after you read the log
TICKET_USD = 25 # flat. never sized off the last win
MAX_OPEN = 4 # 4 x $25 = $100 in the market, max
MAX_ENTRIES_PER_DAY = 6
DAILY_LOSS_HALT_USD = 50 # realized loss today hits this -> no new entries until tomorrow
COOLDOWN_HOURS = 24 # no re-entry into a token you just left
FACTS = { # the rug filter. code only, no model
"max_risk_score": 40, # rugcheck score_normalised, 0-100, higher = riskier
"min_lp_locked_pct": 90, # biggest pool: LP locked or burned
"max_top10_pct": 30, # top 10 holders, pools and lockers excluded
"max_dev_pct": 5, # supply still sitting in the creator wallet
"max_insiders": 0, # insider-flagged holders + insider graph hits
"min_liquidity_usd": 30000,
"max_ticket_share": 0.01, # your ticket is never more than 1% of the pool
"min_age_min": 30, # younger than this, every report is still guessing
"max_age_hours": 72,
}
MIN_CONFIDENCE = 0.7 # Grok says BUY below this -> treated as SKIP
EXIT = {
"stop_pct": -35, # hard stop from entry
"ladder": [(100, 0.50), (200, 0.25)], # +100% sells half, +200% sells a quarter
"trail_pct": 30, # the rest: out when price falls 30% from its peak
"time_stop_hours": 6, # not up 20% after 6 hours -> out, dead money
"time_stop_min_pct": 20,
"liq_drop_pct": 40, # pool liquidity down 40% from entry -> out now
}
LOG = "checks.csv"
POSITIONS = "positions.json"
TRADES = "trades.csv"
FLAT TICKET. the size never grows because the last three won.
THE BOT READS config.py, IT NEVER WRITES IT. a number moves when you move it.
A 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.
# book.py
# positions, trades, limits. the desk's memory lives in two files you can open yourself.
import csv, json, os, sys, time
from config import (POSITIONS, TRADES, TICKET_USD, MAX_OPEN, MAX_ENTRIES_PER_DAY,
DAILY_LOSS_HALT_USD, COOLDOWN_HOURS)
def load() -> dict:
return json.load(open(POSITIONS)) if os.path.exists(POSITIONS) else {}
def save(book: dict):
json.dump(book, open(POSITIONS, "w"), indent=2)
def trades() -> list[dict]:
return list(csv.DictReader(open(TRADES))) if os.path.exists(TRADES) else []
def record(mint, symbol, action, usd, pnl, reason):
new = not os.path.exists(TRADES)
with open(TRADES, "a", newline="") as f:
w = csv.DictWriter(f, fieldnames=["time", "mint", "symbol", "action", "usd", "pnl", "reason"])
if new:
w.writeheader()
w.writerow({"time": time.strftime("%Y-%m-%d %H:%M"), "mint": mint, "symbol": symbol,
"action": action, "usd": round(usd, 2), "pnl": round(pnl, 2), "reason": reason})
def today_pnl() -> float:
day = time.strftime("%Y-%m-%d")
return sum(float(t["pnl"]) for t in trades() if t["time"].startswith(day))
def guard(mint: str) -> str | None:
"""Limits run before any network call. A blocked entry costs nothing."""
day = time.strftime("%Y-%m-%d")
book = load()
if today_pnl() <= -DAILY_LOSS_HALT_USD: return "halted_daily_loss"
if mint in book: return "already_open"
if len(book) >= MAX_OPEN: return "max_open"
opens = [t for t in trades() if t["action"] == "open" and t["time"].startswith(day)]
if len(opens) >= MAX_ENTRIES_PER_DAY: return "max_entries_today"
cutoff = time.strftime("%Y-%m-%d %H:%M", time.localtime(time.time() - COOLDOWN_HOURS * 3600))
if any(t["mint"] == mint and t["action"] == "close" and t["time"] >= cutoff for t in trades()):
return "cooldown"
return None
def open_position(mint, symbol, price, liq_usd, usd=TICKET_USD):
book = load()
book[mint] = {"symbol": symbol, "entry": price, "qty0": usd / price, "qty": usd / price,
"peak": price, "liq0": liq_usd, "opened": time.time(), "steps": 0}
save(book)
record(mint, symbol, "open", usd, 0, "entry")
def sell(mint, frac_of_original, price, reason) -> float:
"""Sells a fraction of the ORIGINAL size (1.0 = everything left). Returns realized pnl."""
book = load()
p = book[mint]
q = min(p["qty"], p["qty0"] * frac_of_original)
pnl = q * (price - p["entry"])
p["qty"] -= q
action = "close" if p["qty"] <= p["qty0"] * 1e-6 else "partial"
record(mint, p["symbol"], action, q * price, pnl, reason)
if action == "close":
del book[mint]
save(book)
return pnl
if __name__ == "__main__":
# live mode: after you buy in your own wallet -> python book.py add <mint> <usd> <price> <liq_usd>
# after you sell by hand -> python book.py close <mint> <price>
cmd = sys.argv[1]
if cmd == "add":
mint, usd, price, liq = sys.argv[2], float(sys.argv[3]), float(sys.argv[4]), float(sys.argv[5])
open_position(mint, mint[:6], price, liq, usd)
elif cmd == "close":
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:
# watch.py
# every open position, every 10 minutes. prints nothing when nothing changed.
import time
from config import EXIT, PAPER
from market import pool
import book
def watch() -> list[str]:
events = []
for mint, p in list(book.load().items()):
try:
m = pool(mint)
except Exception as e:
events.append(f"{p['symbol']}: price feed down ({e}), holding, will retry")
continue
if not m or m["price"] <= 0:
events.append(f"{p['symbol']}: pool gone. treat as rugged, sell what you can")
book.sell(mint, 1.0, 0.0, "pool_gone")
continue
price = m["price"]
b = book.load(); s = b[mint]
s["peak"] = max(s["peak"], price); book.save(b)
pct = (price / s["entry"] - 1) * 100
hours = (time.time() - s["opened"]) / 3600
def out(frac, why):
pnl = book.sell(mint, frac, price, why)
verb = "sold" if PAPER else "SELL NOW in your wallet"
size = "the rest" if frac >= 1 else f"{int(frac * 100)}%"
events.append(f"{s['symbol']}: {verb} {size} at {pct:+.0f}% ({why}), pnl ${pnl:+.2f}")
if m["liq_usd"] < s["liq0"] * (1 - EXIT["liq_drop_pct"] / 100):
out(1.0, "liquidity_pulled"); continue
if pct <= EXIT["stop_pct"]:
out(1.0, "stop"); continue
ladder = EXIT["ladder"]
if s["steps"] < len(ladder) and pct >= ladder[s["steps"]][0]:
out(ladder[s["steps"]][1], f"take_profit_{ladder[s['steps']][0]}")
b = book.load()
if mint in b:
b[mint]["steps"] += 1; book.save(b)
continue
if s["steps"] > 0 and price <= s["peak"] * (1 - EXIT["trail_pct"] / 100):
out(1.0, "trailing_stop"); continue
if hours >= EXIT["time_stop_hours"] and pct < EXIT["time_stop_min_pct"]:
out(1.0, "time_stop")
return events
if __name__ == "__main__":
for e in watch():
print(e)
LIQUIDITY FIRST. a pulled pool beats every other rule, even on a green position.
THE TRAIL ARMS AFTER THE FIRST TAKE. before that, -35% is the only floor.
DEAD MONEY IS A LOSS TOO. six flat hours free the slot for the next token.
SILENCE 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.
# rugcheck.py
# one report per token, trimmed to the fields the desk reads. Grok never sees the raw report.
import requests
from config import FACTS
RC = "https://api.rugcheck.xyz/v1/tokens"
def report(mint: str) -> dict:
r = requests.get(f"{RC}/{mint}/report", timeout=20)
r.raise_for_status()
d = r.json()
token = d.get("token") or {}
known = d.get("knownAccounts") or {}
def is_pool(h): # AMM vaults and lockers sit in top holders, they are not whales
tag = known.get(h.get("owner")) or known.get(h.get("address")) or {}
return str(tag.get("type", "")).upper() in ("AMM", "LOCKER")
holders = [h for h in (d.get("topHolders") or []) if not is_pool(h)]
markets = sorted(d.get("markets") or [],
key=lambda m: ((m.get("lp") or {}).get("baseUSD") or 0)
+ ((m.get("lp") or {}).get("quoteUSD") or 0),
reverse=True)
lp = (markets[0].get("lp") or {}) if markets else {}
supply = token.get("supply") or 0
risks = d.get("risks") or []
return {
"mint": mint,
"symbol": (d.get("tokenMeta") or {}).get("symbol"),
"rugged": bool(d.get("rugged")),
"risk_score": d.get("score_normalised"),
"danger": [x.get("name") for x in risks if x.get("level") == "danger"],
"warn": [x.get("name") for x in risks if x.get("level") == "warn"],
"mint_authority": token.get("mintAuthority", d.get("mintAuthority")),
"freeze_authority": token.get("freezeAuthority", d.get("freezeAuthority")),
"lp_locked_pct": lp.get("lpLockedPct", d.get("lpLockedPct")),
"top10_pct": round(sum(h.get("pct") or 0 for h in holders[:10]), 2) if holders else None,
"dev_pct": round((d.get("creatorBalance") or 0) / supply * 100, 2) if supply else None,
"insiders": sum(1 for h in holders if h.get("insider")) + (d.get("graphInsidersDetected") or 0),
"holders": d.get("totalHolders"),
}
def fact_kill(t: dict) -> str | None:
"""Plain comparisons. First failure wins, the reason gets logged."""
if t["risk_score"] is None: return "no_report"
if t["rugged"]: return "already_rugged"
if t["mint_authority"]: return "mint_open"
if t["freeze_authority"]: return "freeze_open"
if t["danger"]: return "danger:" + t["danger"][0]
if t["risk_score"] > FACTS["max_risk_score"]: return "risk_score"
if t["lp_locked_pct"] is None or t["lp_locked_pct"] < FACTS["min_lp_locked_pct"]:
return "lp_unlocked"
if t["top10_pct"] is None or t["top10_pct"] > FACTS["max_top10_pct"]:
return "top10_heavy"
if t["dev_pct"] is None or t["dev_pct"] > FACTS["max_dev_pct"]:
return "dev_holds"
if t["insiders"] > FACTS["max_insiders"]: return "insiders"
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.
# market.py
# the pool itself: depth, age, price, and whether anyone can actually sell.
import time
import requests
from config import FACTS, TICKET_USD
DS = "https://api.dexscreener.com/tokens/v1/solana"
def pool(mint: str) -> dict | None:
r = requests.get(f"{DS}/{mint}", timeout=15)
r.raise_for_status()
data = r.json()
pairs = data if isinstance(data, list) else (data.get("pairs") or [])
pairs = [p for p in pairs if (p.get("baseToken") or {}).get("address") == mint]
if not pairs:
return None
p = max(pairs, key=lambda x: (x.get("liquidity") or {}).get("usd") or 0)
h1 = (p.get("txns") or {}).get("h1") or {}
born = p.get("pairCreatedAt")
return {
"symbol": (p.get("baseToken") or {}).get("symbol"),
"price": float(p.get("priceUsd") or 0),
"liq_usd": (p.get("liquidity") or {}).get("usd") or 0,
"age_min": round((time.time() * 1000 - born) / 60000) if born else None,
"buys_1h": h1.get("buys", 0),
"sells_1h": h1.get("sells", 0),
"vol_1h": (p.get("volume") or {}).get("h1"),
"url": p.get("url"),
}
def market_kill(m: dict | None) -> str | None:
if not m: return "no_pool"
if m["price"] <= 0: return "no_price"
if m["liq_usd"] < FACTS["min_liquidity_usd"]: return "thin_pool"
if TICKET_USD / m["liq_usd"] > FACTS["max_ticket_share"]: return "ticket_too_big"
if m["age_min"] is None or m["age_min"] < FACTS["min_age_min"]:
return "too_fresh"
if m["age_min"] > FACTS["max_age_hours"] * 60: return "too_old"
if m["buys_1h"] >= 20 and m["sells_1h"] == 0: return "nobody_can_sell"
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:
# grok.py
# the only file that holds the key and the only place the desk talks to xAI.
import os, json, datetime, requests
from config import MODEL
URL = "https://api.x.ai/v1/responses"
def _key() -> str:
k = os.environ.get("XAI_API_KEY")
if not k and os.path.exists(".env"):
for line in open(".env"):
if line.startswith("XAI_API_KEY="):
k = line.split("=", 1)[1].strip().strip('"')
if not k:
raise RuntimeError("no XAI_API_KEY in the environment or in .env")
return k
def _text(resp: dict) -> str:
if isinstance(resp.get("output_text"), str):
return resp["output_text"]
parts = []
for item in resp.get("output") or []:
if item.get("type") == "message":
for c in item.get("content") or []:
if c.get("type") in ("output_text", "text"):
parts.append(c.get("text") or "")
return "\n".join(parts)
def ask(system: str, payload: dict, read_x: bool = False) -> dict:
"""One call, JSON in, JSON out. Anything that isn't JSON raises, and a raise is a SKIP upstream."""
body = {"model": MODEL,
"input": [{"role": "system", "content": system},
{"role": "user", "content": json.dumps(payload)}]}
if read_x:
since = (datetime.date.today() - datetime.timedelta(days=3)).isoformat()
body["tools"] = [{"type": "x_search", "from_date": since}]
body["max_turns"] = 2 # two rounds of searching, then it has to answer
r = requests.post(URL, timeout=180, json=body,
headers={"Authorization": f"Bearer {_key()}"})
r.raise_for_status()
text = _text(r.json())
start, end = text.find("{"), text.rfind("}")
if start == -1:
raise ValueError(f"no json in answer: {text[:200]}")
return json.loads(text[start:end + 1])
save as prompts.py. this is the file you'll actually reread:
# prompts.py
# the one Grok prompt on the desk. this is the file you will actually reread.
JUDGE_PROMPT = """
You are RUG DESK. A Solana memecoin just passed every hard check in code:
authorities, LP, holders, dev wallet, insiders, pool depth, age.
Do not re-check those numbers. You decide if one flat ticket goes in.
You never size, you never pick exits.
INPUT: {"token": {...rugcheck facts...}, "pool": {...dexscreener facts...}}
STEP 1. Search X for the ticker and for the contract address, last 72 hours.
STEP 2. Judge only what code can't see:
- do the warn-level risks stack into something worse together
- who posted first: the dev, a real account with history, or a wave of fresh accounts
- organic or bought: same wording, same minute, accounts with no past
- is the contract address in those posts the same one as in INPUT
- has this ticker name or this dev been tied to a rug before
RETURN ONLY JSON:
{"verdict": "BUY" | "SKIP", "confidence": 0.0-1.0,
"why": "two lines max", "x_read": "one line on what X looks like",
"main_risk": "one line"}
Zero posts on X is an answer: nobody knows it exists. SKIP.
Another contract address under the same ticker on X: copycat risk. SKIP.
NEVER answer BUY when a field you needed is null.
NEVER mention price targets. You don't know them.
"""
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:
# check.py
# one contract address in, one verdict out. runs every time a token crosses your desk.
import csv, json, os, sys, time
from config import LOG, MIN_CONFIDENCE, PAPER, TICKET_USD
from rugcheck import report, fact_kill
from market import pool, market_kill
from grok import ask
from prompts import JUDGE_PROMPT
import book
def log(row: dict):
new = not os.path.exists(LOG)
with open(LOG, "a", newline="") as f:
w = csv.DictWriter(f, fieldnames=["time", "mint", "symbol", "stage", "verdict",
"confidence", "reason"])
if new:
w.writeheader()
w.writerow({k: row.get(k) for k in w.fieldnames})
def check(mint: str) -> dict:
if (g := book.guard(mint)): # limits, free
return {"stage": "limits", "verdict": "SKIP", "reason": g}
try:
token = report(mint) # rug filter, free
except Exception as e:
return {"stage": "facts", "verdict": "SKIP", "reason": f"rugcheck failed: {e}"}
if (k := fact_kill(token)):
return {"stage": "facts", "verdict": "SKIP", "reason": k, "symbol": token["symbol"]}
try:
m = pool(mint) # pool, free
except Exception as e:
return {"stage": "pool", "verdict": "SKIP", "reason": f"dexscreener failed: {e}"}
if (k := market_kill(m)):
return {"stage": "pool", "verdict": "SKIP", "reason": k, "symbol": token["symbol"]}
try:
ans = ask(JUDGE_PROMPT, {"token": token, "pool": m}, read_x=True) # the only paid step
except Exception as e:
return {"stage": "grok", "verdict": "SKIP", "reason": f"grok failed: {e}"}
verdict, conf = ans.get("verdict"), ans.get("confidence") or 0
if verdict == "BUY" and conf < MIN_CONFIDENCE:
verdict = "SKIP"
ans["why"] = f"low confidence {conf}"
return {**ans, "stage": "grok", "verdict": verdict, "symbol": m["symbol"] or token["symbol"],
"reason": ans.get("why"), "price": m["price"], "liq_usd": m["liq_usd"], "url": m["url"]}
if __name__ == "__main__":
mint = sys.argv[1].strip()
v = check(mint)
log({"time": time.strftime("%Y-%m-%d %H:%M"), "mint": mint, **v})
if v["verdict"] == "BUY":
if PAPER:
book.open_position(mint, v["symbol"], v["price"], v["liq_usd"])
v["action"] = f"PAPER BUY ${TICKET_USD} at {v['price']}"
else:
v["action"] = (f"APPROVAL NEEDED: buy ${TICKET_USD} of {v['symbol']} in your own wallet, "
f"then run: python book.py add {mint} {TICKET_USD} <fill price> {v['liq_usd']}")
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):
{
"verdict": "SKIP",
"confidence": 0.78,
"why": "passes every hard check, but the first 30 posts came from accounts created this week, same wording.",
"x_read": "one real account, a wave of fresh ones, no CA mismatch",
"main_risk": "bought hype, the buyers leave when the posting stops",
"stage": "grok",
"symbol": "EXAMPLE",
"reason": "passes every hard check, but the first 30 posts came from accounts created this week, same wording."
}
FACTS BEFORE OPINIONS. Grok never decides whether a mint is open.
A FAILED CALL IS A SKIP. Rugcheck down, DexScreener down, Grok down, bad JSON: SKIP.
NULL IS NOT CLEAN. a field that didn't come back kills the token, it never passes it.
ONE VERDICT PER TOKEN. never re-run until it says BUY. if you disagree, you disagree with config.py.
READ 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:
Every 10 minutes, in /workspace/desk run python watch.py.
If it prints nothing, say nothing. If it prints anything, post it here word for word.
Every 30 minutes, in /workspace/desk run python scan.py.
Post only BUY verdicts: symbol, confidence, x_read.
If Rugcheck or DexScreener is down, say which one once, not every run.
Every day at 21:30, in /workspace/desk run python report.py and post the output here.
Add one thing that worked today and one number in config.py you would question, with the reason.
Never change that number yourself.
the two files those routines run. save as report.py:
# report.py
# the evening report. everything in it comes from checks.csv and trades.csv, nothing from memory.
import csv, os, time
from collections import Counter
from config import LOG, PAPER
import book
day = time.strftime("%Y-%m-%d")
checks = [r for r in csv.DictReader(open(LOG))] if os.path.exists(LOG) else []
checks = [r for r in checks if r["time"].startswith(day)]
done = [t for t in book.trades() if t["time"].startswith(day) and t["action"] != "open"]
by_stage = Counter(r["stage"] for r in checks if r["verdict"] == "SKIP")
buys = [r for r in checks if r["verdict"] == "BUY"]
wins = [t for t in done if float(t["pnl"]) > 0]
reasons = Counter(r["reason"] for r in checks if r["stage"] in ("facts", "pool"))
print(f"EVENING REPORT {day} {'(PAPER)' if PAPER else '(LIVE)'}")
print(f"checked: {len(checks)}")
print(f"killed by rug filter: {by_stage['facts']} | by pool: {by_stage['pool']} | "
f"by Grok: {by_stage['grok']} | by limits: {by_stage['limits']}")
print(f"bought: {len(buys)} | open now: {len(book.load())}")
print(f"realized pnl today: ${book.today_pnl():+.2f}")
if done:
print(f"green exits: {len(wins)}/{len(done)}")
best = max(done, key=lambda t: float(t["pnl"]))
worst = min(done, key=lambda t: float(t["pnl"]))
print(f"best: {best['symbol']} ${float(best['pnl']):+.2f} | worst: {worst['symbol']} ${float(worst['pnl']):+.2f}")
if reasons:
top, n = reasons.most_common(1)[0]
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:
# scan.py
# optional lead source: fresh Solana tokens that just paid for a DexScreener profile.
# a paid profile is not a quality signal, it just means the dev spent money. the filter decides.
import csv, os, subprocess, sys
import requests
from config import LOG
seen = {r["mint"] for r in csv.DictReader(open(LOG))} if os.path.exists(LOG) else set()
r = requests.get("https://api.dexscreener.com/token-profiles/latest/v1", timeout=15)
r.raise_for_status()
fresh = [t["tokenAddress"] for t in r.json()
if t.get("chainId") == "solana" and t.get("tokenAddress") not in seen]
for mint in fresh[:5]: # five per run, so one busy hour can't drain the API budget
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:
Ask first before any wallet connection, signature, swap or transfer.
Ask 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.
401 from xAI -> bad key. Take over, rewrite .env, run the curl from step 03 again.
400 / 404 from xAI -> model name changed. fix MODEL in config.py, nothing else.
429 from xAI -> rate limit. that token is a SKIP, the next run moves on. never loop it.
error about x_search -> your MODEL can't search X. pick one that can on console.x.ai -> Models.
Grok answers in prose -> ValueError, logged, SKIP. never turn a paragraph into a trade.
rugcheck 404 or empty -> too fresh to index. SKIP. fresh is not clean.
lp_unlocked on everything -> some pool types have no LP token at all. open one on rugcheck yourself
before you touch that number, and change it in writing.
no_pool -> not trading on a DEX yet, or not Solana. SKIP.
routines went quiet -> Grok Bot pauses routines after you've been away a long time.
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.
cost_per_token = input_tokens / 1_000_000 * price_per_M_input
+ output_tokens / 1_000_000 * price_per_M_output
+ posts_fetched / 1_000 * 5 # X search: $5 per 1,000 posts fetched
max_turns = 2 keeps the search to two rounds, so posts_fetched stays small.
every response reports its own x_posts_fetched, so day one tells you the real number.
price: [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
沿着这个主题继续读
从一篇文章,找到相关的教程、经验和阅读路径。












