好文集ChatGPT Plus¥119

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.

#checkkills whenwhy
1already ruggedRugcheck marks it ruggedsomeone already did it
2mint authoritystill openthe dev can print new supply into your bag
3freeze authoritystill openthe dev can freeze your tokens, you can't sell
4danger flagsany danger-level riskRugcheck already saw something you'd have to prove wrong
5risk scoreabove 40/100small warnings add up
6LP lockbiggest pool under 90% locked or burnedunlocked liquidity is a pull button
7top 10 holdersover 30%, pools and lockers excludedten wallets can end your trade together
8dev walletcreator still holds over 5%the dev is the first seller
9insiders / bundlesany insider holder or insider graph hitwallets funded together sell together
10pool depthunder $30k liquidityyou'd be someone's exit
11ticket shareyour $25 is over 1% of the poolyour own buy moves the price
12ageunder 30 min or over 72htoo fresh for any report, or the move already happened
13sells exist20+ buys in the last hour, zero sellsthat's the shape of a honeypot
14X read (Grok)no posts, copycat CA, bought hype, rug historythe 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

全文完
查看原文

沿着这个主题继续读

从一篇文章,找到相关的教程、经验和阅读路径。

Grok BotGrok Bot 开通、环境配置与多 Bot 协作,从入门到搭建自己的 Agent 团队。