real-money worker scaffolding (LIVE_ROLLOUT Phase 3 prep)

- start.sh: COPYBOT_ROLE=live — unarmed (missing LIVE_PRIVATE_KEY /
  LIVE_FUNDER_ADDRESS / LIVE_CONFIRM) idles in geocheck, no clone, no
  book, no orders; armed runs config.live.example.json + own state file
  copybot_state.live.json, --live, poll mode (webhook stays on paper).
- confirm_live: accepts the phrase from LIVE_CONFIRM env — the USER
  types it into flyctl secrets set (rule 0.7 human checkpoint, headless);
  wrong value aborts, unset disarms at next boot.
- config.live.example.json: Set E + pinned floors (mirrors paper),
  bankroll $50 @ 10% = $5 stakes, caps untouched (rule 0.6), leaked
  Discord webhook URL removed (ROTATE it — was committed publicly).
- fly.live.toml: separate app wwf-copybot-live, arn, no inbound, no
  checks, role env baked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-08 23:17:35 -04:00
parent b433448279
commit 58b89c106a
4 changed files with 99 additions and 40 deletions
+41 -40
View File
@@ -1,26 +1,28 @@
{
"mode": "live",
"_comment": "Live-test config, SECRET-FREE and committed. Two uses: (1) cp -> config.live.json, fill live.private_key/funder_address, run on a geo-TRADABLE Mac; (2) the Fly live worker reads THIS file directly and gets the key from LIVE_PRIVATE_KEY/LIVE_FUNDER_ADDRESS env vars (copybot 1.2). $5/trade test caps \u2014 see LIVE_ROLLOUT.md before raising anything.",
"bankroll_usd": 250.0,
"bankroll_pct": 0.02,
"_comment": "Live-test config, SECRET-FREE and committed. The Fly live worker (wwf-copybot-live, COPYBOT_ROLE=live) reads THIS file and takes secrets from env (LIVE_PRIVATE_KEY/LIVE_FUNDER_ADDRESS + LIVE_CONFIRM to arm). Mac use: cp -> config.live.json and fill live.*. RULES: bankroll_usd MUST equal the actual USDC deposit at arm time (the 1.4 cash-anchor checks state cash vs chain); risk caps are LIVE_ROLLOUT rule 0.6 \u2014 never raise before Phase 6; follow set mirrors copybot.paper.json (Set E) so paper and live books stay comparable.",
"bankroll_usd": 50.0,
"bankroll_pct": 0.1,
"price_guard_pct": 0.05,
"taker_fee_rate": 0.03,
"discord_webhook": "https://discord.com/api/webhooks/1515187084409704660/VbKWKIIo2ks1sSs2r1cr9M3OSIj_6oKDOBRp7Z5DneQCyk8AnPiyxVKelxensLP-aHaH",
"discord_webhook": "",
"follow": {
"buy_only": true,
"min_their_usd": 25.0,
"per_wallet_min_usd": {
"0xe8ca3f758c93f44f3ec210542ab78afb7c0bcccb": 123.27,
"0xf3488e52ac2d7f0628b04481db5a5b0446f0e543": 727.08,
"0x86c878cde72660ec52f5e6f0f0438b76de8fc867": 990.45,
"0x41558102a796ba971c7567cad41c307e59f8fa41": 231.0,
"0xd96750bf8d941a8186e592b0ae6e096da66aa266": 115.27
"0x41558102a796ba971c7567cad41c307e59f8fa41": 258.51,
"0xd96750bf8d941a8186e592b0ae6e096da66aa266": 122.32,
"0xe8ca3f758c93f44f3ec210542ab78afb7c0bcccb": 126.84,
"0xbadaf319415c17f28824a43ae0cd912b9d84d874": 44.98,
"0xd7d36345c4aab150e59577e360696b01d01d698b": 304.34,
"0x921433c93558b9a4ba807ec824d02aad7ea2ddbf": 295.32,
"0xa1d57d329227c75b12b09f927fb3d6d6ef8f1343": 320.95
},
"min_entry": 0.0,
"max_entry": 0.95,
"class_pct": {
"volume": 0.02,
"whale": 0.02
"volume": 0.1,
"whale": 0.1
}
},
"risk": {
@@ -42,50 +44,49 @@
"auto_redeem": true,
"rpc_url": ""
},
"stake_cap_usd": 250.0,
"stake_cap_usd": 50.0,
"wallets": [
{
"wallet": "0xe8ca3f758c93f44f3ec210542ab78afb7c0bcccb",
"name": "Kruto2027",
"class": "volume"
},
{
"wallet": "0x86c878cde72660ec52f5e6f0f0438b76de8fc867",
"name": "fortuneking",
"class": "volume"
},
{
"wallet": "0x41558102a796ba971c7567cad41c307e59f8fa41",
"name": "LSB1",
"class": "volume"
"class": "volume",
"floor": 258.51
},
{
"wallet": "0xd96750bf8d941a8186e592b0ae6e096da66aa266",
"name": "imwalkinghere",
"class": "volume"
},
{
"wallet": "0xfc81760d44a21acc9fd4b749a5bf9a9b2eeae072",
"name": "iohihoo",
"class": "volume"
},
{
"wallet": "0xf5fe759cece500f58a431ef8dacea321f6e3e23d",
"name": "Stavenson",
"class": "volume",
"floor": 4759.62
"floor": 122.32
},
{
"wallet": "0x73afc8160c17830c0c7281a7bf570c871455b880",
"name": "0x4bFb-whale",
"wallet": "0xe8ca3f758c93f44f3ec210542ab78afb7c0bcccb",
"name": "Kruto2027",
"class": "volume",
"floor": 1090.46
"floor": 126.84
},
{
"wallet": "0x72e1597864456eda62878413cf3e60c332e4a45d",
"name": "ArbTrader",
"wallet": "0xbadaf319415c17f28824a43ae0cd912b9d84d874",
"name": "0xbadaf319",
"class": "volume",
"floor": 168.28
"floor": 44.98
},
{
"wallet": "0xd7d36345c4aab150e59577e360696b01d01d698b",
"name": "gkmgkldfmg",
"class": "volume",
"floor": 304.34
},
{
"wallet": "0x921433c93558b9a4ba807ec824d02aad7ea2ddbf",
"name": "AIcAIc",
"class": "volume",
"floor": 295.32
},
{
"wallet": "0xa1d57d329227c75b12b09f927fb3d6d6ef8f1343",
"name": "1kto1m",
"class": "volume",
"floor": 320.95
}
],
"feed_path": "live/copybot_live_real.json",
+12
View File
@@ -599,6 +599,18 @@ def confirm_live(cfg):
f" · daily cap ${cfg['risk']['daily_spend_cap_usd']:.0f}")
print(f" Watching {len(cfg['watchlist'])} wallets.")
print("=" * 64)
# Headless arm (Fly live worker): the USER types the exact phrase into
# `flyctl secrets set LIVE_CONFIRM="…"` — still a human checkpoint, never
# baked into config or code (LIVE_ROLLOUT rule 0.7). Known property: while
# the secret stays set, restarts RE-ARM automatically (desired during the
# days-long Phase 5 matrix); `flyctl secrets unset LIVE_CONFIRM` disarms
# at the next boot, and any wrong value aborts instead of prompting.
env_phrase = os.environ.get("LIVE_CONFIRM")
if env_phrase is not None:
if env_phrase.strip() == CONFIRM_PHRASE:
print("confirmed via LIVE_CONFIRM env — armed.")
return
sys.exit("Aborted — LIVE_CONFIRM is set but does not match the phrase.")
typed = input(f'Type "{CONFIRM_PHRASE}" to proceed (anything else aborts): ')
if typed.strip() != CONFIRM_PHRASE:
sys.exit("Aborted — not confirmed.")
+20
View File
@@ -0,0 +1,20 @@
# Fly.io config for the REAL MONEY copybot worker — a SEPARATE app from the
# paper worker (wwf-copybot) so the two books never share a process, state
# file, or failure mode (LIVE_ROLLOUT Phase 3.3). Region rules: see fly.toml.
#
# Poll mode only (60s; the Alchemy push webhook stays on the paper app), no
# inbound services, no health checks — the unarmed machine idles in
# geocheck --idle until the USER sets LIVE_PRIVATE_KEY + LIVE_FUNDER_ADDRESS
# + LIVE_CONFIRM (the typed confirmation phrase) via flyctl secrets set.
app = "wwf-copybot-live"
primary_region = "arn"
[build]
dockerfile = "fly.Dockerfile"
[env]
COPYBOT_ROLE = "live"
[[vm]]
size = "shared-cpu-1x"
memory = "512mb"
+26
View File
@@ -39,6 +39,21 @@ fi
python3 "$(dirname "$0")/geocheck.py" \
|| echo "⚠ geo-gate BLOCKED/unknown here — fine for paper, do NOT go live from this box"
# COPYBOT_ROLE=live -> the REAL MONEY worker (separate Fly app
# wwf-copybot-live; NEVER convert the paper worker — the two roles must never
# share a process or a state file). Unarmed = missing any of
# LIVE_PRIVATE_KEY / LIVE_FUNDER_ADDRESS / LIVE_CONFIRM -> idle harmlessly
# (no clone, no book, no orders) so the machine can exist safely before
# Phase 2 funding. Arming is three `flyctl secrets set` calls by the USER;
# copybot.py additionally re-checks the geo-gate fatally and validates the
# phrase itself (LIVE_ROLLOUT 0.7, 1.5).
if [ "${COPYBOT_ROLE:-paper}" = "live" ]; then
if [ -z "${LIVE_PRIVATE_KEY:-}" ] || [ -z "${LIVE_FUNDER_ADDRESS:-}" ] || [ -z "${LIVE_CONFIRM:-}" ]; then
echo "live role UNARMED (need LIVE_PRIVATE_KEY + LIVE_FUNDER_ADDRESS + LIVE_CONFIRM) — idling"
exec python3 "$(dirname "$0")/geocheck.py" --idle
fi
fi
: "${GITHUB_TOKEN:?set GITHUB_TOKEN (PAT with repo contents read+write)}"
REPO_URL="https://x-access-token:${GITHUB_TOKEN}@github.com/jaxperro/winning-wallet-finder.git"
DIR="${COPYBOT_DIR:-/tmp/wwf}"
@@ -49,6 +64,17 @@ cd "$DIR"
git config user.name "copybot[bot]"
git config user.email "copybot@users.noreply.github.com"
# live role, ARMED: own config (committed template + env secrets), own state
# file, own feed/fills (config paths), ALWAYS poll mode — the Alchemy push
# webhook stays pointed at the paper app (LIVE_ROLLOUT Phase 3.3).
if [ "${COPYBOT_ROLE:-paper}" = "live" ]; then
exec python3 copybot.py \
--config config.live.example.json \
--state copybot_state.live.json \
--live \
--poll "${POLL_SECONDS:-60}"
fi
# paper config is committed (no secrets); state resumes from the last commit.
# ALCHEMY_SIGNING_KEY set -> push mode (webhook server); else 60s poll mode.
if [ -n "${ALCHEMY_SIGNING_KEY:-}" ]; then