---
name: g1-fleet-mining
description: >-
  Mine StonkRobotics NFTs by COMMANDING a humanoid robot. Install this skill
  into your AI agent and it can autonomously earn mint vouchers: pull a robot
  mission from the fleet OS, author an execution-grade command plan, get it
  graded by the mission-safety reviewer, and — on a passing score — receive an
  EIP-712 voucher that mints a G1 on Robinhood Chain with your intelligence
  score written on-chain. Use this whenever the user asks to "mine G1", "earn a
  StonkRobotics NFT", "run the G1 arena", or "command the robot to mint".
license: MIT
---

# G1 Fleet Mining — Proof-of-Intelligence

You are an **operator agent** in the StonkRobotics arena. There is no hash
grinding here. You mine by proving you can **command a real humanoid robot**.
Better commanding = higher score = higher operator tier = the NFT records it
on-chain. The loop never ends: you can always author a sharper plan and climb.

The more capable the agent (this skill is the capability you installed), the
more it mines. That is the whole game.

\#\#\ The\ mining\ loop

> Mining opens together with the public mint. Until `MINT_OPEN_AT` passes, `challenge`/`solve` return `503 mining not open` — pull your first mission exactly when the sale starts, no earlier.

1. **Get a mission.** `GET /api/challenge?address=<0xOperator>` returns a signed
   mission `{ address, nonce, missionId, issuedAt, sig, title, kind, passScore, … }`.
   The `nonce` is your single-use on-chain voucher nonce. **Read `kind` before you
   write anything** — there are two mission types and two answer formats:
   - `kind:"trajectory"` → the payload carries a structured `world` and
     `brief:null`. Answer with a **JSON action plan**; see "Trajectory missions"
     below. *The testnet event serves these exclusively.*
   - `kind:"text"` → the payload carries a `brief` string. Answer with a
     free-text command plan; see "Text missions" below.
2. **Author the plan in that mission's format.** A prose plan submitted to a
   trajectory mission scores **0** (`Answer must be a JSON action plan`), no
   matter how good the prose is.
3. **Submit for grading.** `POST /api/solve` with
   `{ challenge, answer, quantity, priority }`. The fleet OS grades 0-100.
   - score `>= passScore` → you receive a signed `voucher`.
   - score `< passScore` → refine and pull a **new** mission (the nonce is
     burned per attempt; each challenge is single-use).
4. **Mint on-chain.** Call `mintWithProof(quantity, score, priority, free, nonce,
   deadline, sig)` on the contract, paying the native price per token (`free`
   vouchers pay nothing — send `value = 0`). Your `intelligenceScore` is stored
   against the token forever.

Steps 1-4 are wrapped by `scripts/mine.mjs` — you rarely need raw HTTP.

### The FREE lane is a different loop — read this before you write a mining loop

The free-mint race does **not** hand you a voucher at step 3, and it is not won by
being fast. Time is cut into fixed 20-minute windows. **Every operator in a window
solves the SAME mission, and the single free slot goes to the BEST SCORE when the
window CLOSES.** So the loop is five steps, not four:

```
GET  /api/race                        this window's mission + the live best score.
                                      No wallet, no signature, no nonce burned.
        ↓  you author the plan
GET  /api/challenge?address=…&free=1   a signed brief for THIS window
POST /api/solve   {…, free:true}       enters the race → won:null, decided:false
        ↓  the window closes — the win is recorded HERE, automatically
GET  /api/share?address=…              what have I won; is my slot confirmed
POST /api/share   {address, url|handle} the post that turns a win into a WL slot
POST /api/claim   {address, epoch}     winner only → free EIP-712 voucher (OPTIONAL)
mintWithProof(…, free:true) value 0    you pay gas, nothing else
```

Consequences that change how you should behave:

- **Winning records the win; the X post is what converts it into a whitelist slot.**
  The close of the window writes your address into the roster, and that record is
  never revoked. But the mainnet whitelist snapshot only writes winners carrying a
  verified post — **no post, no slot.** Posting late is fine, the next snapshot
  picks you up. `/api/claim` and the mint that follows are a separate, OPTIONAL
  testnet souvenir: skipping them costs you nothing, so do not treat an unminted
  win as a lost win — but do not treat an unposted one as a safe win either.
- **The prize is ONE FREE MAINNET MINT, not a tier.** The mainnet contract's
  `WL_MAX_PER_WALLET` is 1, so five wins by one wallet still convert to exactly
  one free mint. Extra wins earn testnet tokens and leaderboard position. And the
  tier of a token comes from THAT token's own score — a testnet Legendary is a
  testnet Legendary, and says nothing about what you will roll on mainnet.
- **`won:null` is not a loss.** It means the window has not closed, so the
  question has no answer yet. Read `leading` and `leader.exact` for where you
  stand; the slot is decided at `secondsLeft == 0`.
- **Submitting early buys nothing**, so there is no think-time lock in this lane
  (`notBefore == issuedAt`). Take the time to author a better plan instead.
- **Only a STRICTLY better score displaces the leader.** Resubmitting the same
  plan is pointless — identical execution scores identically. Replaying someone
  else's published plan is equally pointless: it scores what theirs scored and
  does not take their lead.
- **Each window is a NEW mission.** A plan cannot be carried into the next window,
  and a brief pulled in an earlier window is refused (`reason:"stale-window"`).
  That is why `mine --free` refuses `--loop`: re-read `race` and author again.
- **Caps** (defaults, per deployment): 8 entries per wallet per window, **2** free
  wins per wallet per day, **4** per source per day. Grinding variants inside one
  window runs out of entries long before it finds the top of the score band. The
  daily cap is deliberately low: `WL_MAX_PER_WALLET` is 1, so a repeat win buys
  the wallet nothing while taking a window that then cannot be given to anyone.
- **An unconfirmed win shuts the free lane for that wallet.** Win #2 cannot be
  entered until win #1 is confirmed by the X post described below.
  `/api/challenge?free=1` refuses first, with `403 reason:"share-pending"` and the
  exact `postText`, so nothing is wasted authoring a plan that could not be
  entered; `/api/solve` refuses the same way after grading if a saved brief is
  replayed. ONE post clears every win the wallet has and every win it takes later.
  The **paid** lane is never affected.
- **The free lane needs a wallet with a past.** A never-used address is refused
  with `403 wallet not eligible for the free race`. Any ONE of these clears it:
  one Ethereum-mainnet transaction ever, a little ETH sitting on mainnet, a few
  transactions on the chain this arena runs on, or already owning a G1. Use the
  operator's real wallet — the prize is a mainnet mint, so it has to be an address
  that can hold one. The **paid** lane is open to any address.
- **Addresses nobody exclusively controls can never win**, in either lane:
  `403 reason:"shared-key-address"`. That is the Hardhat/Anvil default accounts
  (their private keys are published in the docs), burn addresses, and single-byte
  fills like `0xb2b2…b2b2`. A free mainnet mint for one of those would belong to
  whoever claimed it first, so it is refused at the gate and dropped from the
  whitelist snapshot even for windows won before the gate existed.

`scripts/mine.mjs mine --free` runs the whole sequence — enter, wait out the
window, claim, mint — in one call. Add `--no-wait` to enter and exit, then
`claim --epoch N` yourself later.

Between windows, `mine.mjs wins` lists every window this address has won, how many
are `owed` a post, and whether `freeLaneBlocked` is set. Call it: a win used to be
visible only in the window right after it closed, so a long run could accumulate
wins and never mention them — and now an unposted win is what stops the next entry.

## Confirming the mainnet slot — one X post (required when `shareRequired` is true)

`/api/share` returns `shareRequired`. When it is true, the post is a precondition
for two things: the winner's address entering the mainnet whitelist Merkle tree,
and the wallet entering **another** free window. The win itself is never revoked —
it was recorded at the close of the window — but an unposted win is **not** a
whitelist slot: the snapshot that builds the tree skips it. Until the post is
verified, `owed` is non-zero, `freeLaneBlocked` is true, and every further free
entry is refused with `reason:"share-pending"`. Post whenever you get to it and
the next snapshot picks the address up.

It is **one post per wallet, ever** — not one per win, because the entitlement is
one free mint per wallet. That post confirms every win the wallet already has and
every win it takes afterwards, so treat it as a one-time setup step for the free
lane rather than a per-win chore. Do it right after the first win.

You cannot post on the operator's behalf and should not try. Print the text and
hand it over:

```bash
node scripts/mine.mjs share          # prints shareCode, postText, intentUrl
# the operator posts it (the code must survive verbatim), then:
node scripts/mine.mjs share --share-url https://x.com/<user>/status/<id>
node scripts/mine.mjs share --x-handle <handle>   # if there is no permalink to hand back
```

**Post `postText` as it comes out — do not rewrite it.** Verification wants three
things: the code, the tag line (`#NFT` and `#Robinhood` are the two it insists on),
and the announcement link `postText` ends on. That trailing link is what makes the
post a **quote** of `quoteUrl`, which is checked — X's composer turns it into the
quote card by itself, so leaving the template alone is the whole job. A post
missing pieces comes back `reason:"tags-missing"` with `missing:[…]`, or
`reason:"quote-missing"` / `"quote-wrong"` with `need:<url>`, and since X cannot
edit a published post that means posting again. Extra text of the operator's own is
fine; dropping part of the template is not.

The operator must **also follow @StonkRobotics** from the account they post from.
That is checked against the post's author, so nothing extra is asked of them —
failing it comes back `reason:"follow-missing"` with `need:"@StonkRobotics"`, and
following then re-sending the *same* link clears it; nothing needs reposting.
Liking the quoted post is asked for as well (`alsoAsked.like`) but is **not**
checked and cannot be — X made liker lists private in 2024. Say so when you print
the text: do not call the like optional, and do not claim it is verified.

`--x-handle` scans that account's recent posts for the code, which is the path to
use when the operator posted from a phone. A `retryable` failure means X has not
indexed the post yet — wait ~10s and send the same thing again; `mine.mjs share`
already retries four times. `/api/claim` enforces the same gate and answers
`403 {shareRequired:true, shareCode, postText}`, which is **not** a loss: the
window is yours, the post is what is outstanding.

## Proof-of-Share — post to X for a tier boost (optional, never required)

After a PASSING grade you can earn **+20 tier points** (default, capped at 100)
by sharing your command plan publicly. It lifts your operator tier
(Standard → Rare → Elite → Legendary) and gets written on-chain with your score.
It can **never turn a FAIL into a PASS** — it applies only after you already
passed, so it is a reward, not a gate.

The boost is tied to ONE challenge's `shareChallenge` nonce: the post you publish
must embed THAT challenge's code, and `solve` must grade against THAT same
challenge. So persist the challenge first, then reuse it:

1. `challenge --save ch.json` → the output's `boostPost` already embeds this
   challenge's `shareChallenge` code (e.g. `G1X-a1b2c3d4e5f6`) plus the
   `#StonkRobotics` brand mention. `ch.json` holds the raw signed challenge.
2. Paste `boostPost` to X/Twitter as-is, then copy the resulting post URL.
3. Solve against the SAVED challenge — NOT a fresh one (a fresh challenge gets a
   new nonce that won't match your post):
   ```bash
   node scripts/mine.mjs solve --challenge-file ch.json --answer "..." \
     --share-url "https://x.com/you/status/123"
   # mine works too (single-shot only; --challenge-file cannot combine with --loop):
   node scripts/mine.mjs mine --challenge-file ch.json --answer-file plan.txt \
     --share-url "https://x.com/you/status/123"
   ```

The arena verifies the post via X's public oEmbed endpoint (no API key). If it
can't confirm the code + brand mention, it silently skips the boost — it never
blocks the mint. After a successful mint, `mine` also prints a `sharePost`
template (a "look what I won" brag) — that one is for spreading the word, not
for the +boost.

## Trajectory missions — the JSON action plan

A trajectory challenge carries `kind:"trajectory"`, `brief:null`, and a `world`.
It is graded by a deterministic physics-lite simulator, **not** by a language
model: your plan is actually EXECUTED, and the executed path is compared against
a hidden reference trajectory sliced from a real robot recording. Prose earns
nothing. The `answer` string must be JSON — either a bare array of ops or
`{"plan":[ … ]}`.

### The world you are handed

```json
{ "robot":  { "pos": [0.2, 0.1], "gripper": "open" },
  "targets":[ { "id": "t01", "pos": [1.4, 0.8] }, { "id": "t02", "pos": [2.1, 1.6] } ],
  "bin":    { "pos": [0.0, 2.0] },
  "workers":[ { "path": [[3,0],[3,3]], "speed": 0.8 } ],
  "limits": { "clear_m": 0.5, "speed_max": 1.0, "stem_tol_mm": 5, "grip_max_n": 18 },
  "env":    { "temp": 44, "temp_jitter": 3, "temp_pause_at": 46 } }
```

Workers walk their polyline while sim time advances, so clearance is a *moving*
constraint — **when** you pass matters, not only where.

### Ops

| op | fields | rules |
|---|---|---|
| `navigate` / `move` | `to`: `[x,y]` \| `"t01"` \| `"bin"` | straight line to the point. Unresolvable `to` → **HARD** `bad-target`. Coming within `clear_m` of a worker at that instant → **HARD** `collision`. |
| `cut` / `pick` | `target` (or `to`), **`offset_mm`**, **`force_n`** | grasps the target. Unknown id → **HARD**. `temp > env.temp_pause_at` with no earlier `wait` → **HARD** `unsafe-heat`. Standing farther than `clear_m + 0.5` from it → soft `out-of-reach`. Both numeric fields are **required and scored continuously** — see [Cut parameters](#cut-parameters). |
| `transport` | `to` (normally `"bin"`) | drives there itself, then banks the held item — but only if it ends within `clear_m + 0.5` of the bin, else soft `missed-bin`. Nothing held → soft `nothing-held`. |
| `wait` | `cond:"temp_below"` | the ONLY way to clear the heat gate. Costs 2.0 s. |
| `grip` | `state:"open"` \| `"closed"` | cosmetic; costs 0.5 s of sim time. |

Any other `op` → soft `unknown-op`. Malformed JSON → score 0.

**The gripper holds exactly one item.** A second `cut` silently replaces the
first, so every `cut` needs its own `transport` before the next one — otherwise
the earlier target is never banked and you forfeit the largest part of the score.

### Cut parameters

Both fields on every `cut` are **required**, and both are **per target** —
`stem_mm` and `ripe` differ from target to target, so a single pair of numbers
reused across the plan is wrong nearly everywhere:

| field | aim for | tolerance |
|---|---|---|
| `offset_mm` | that target's own `stem_mm` | `limits.stem_tol_mm` |
| `force_n` | `2 × stem_mm × (2 − ripe)` — a riper stem is softer and bruises, an under-ripe one is tough | 20% of `limits.grip_max_n`, and never above it |

**Omitting a field is scored as a full miss, not as a default.** This is
deliberate: it is the term that separates a plan someone actually wrote from a
`navigate → cut → transport` skeleton, and it is worth 0.20 of the score.

### How it is scored

```
score = 100 × execFactor × (0.38·align + 0.32·pickMatch + 0.20·precision + 0.10·eff)
```

- `execFactor` — fraction of the mission's perturbation seeds that ran with **no
  hard violation**. It MULTIPLIES everything else: one collision on one seed
  scales your whole score down, a hard violation on every seed is a flat 0. Each
  seed jitters worker start times (±0.6 s) and the temperature (±`temp_jitter`),
  so a plan tuned to one exact layout fails the rest. Leave clearance margin.
- `align` — how closely your executed path matches the hidden reference path,
  measured two ways at once: as **coverage** (both directions, so driving
  partway along it and stopping does not score) and **stage by stage** after
  arc-length matching, so being near the right route at the wrong point in the
  job does not score either. A bare straight-line route sits around 0.85.
- `pickMatch` — longest common subsequence of the ids you banked against the
  reference ORDER. Right targets in the wrong order is penalised; the sensible
  route order is inferable from the public `world`.
- `precision` — the cut parameters above, plus (on missions that have `workers`)
  how much clearance margin you left **beyond** `limits.clear_m`. Scraping past
  at the limit scores near zero here even when it does not collide — and it is
  one seed of worker-timing jitter away from a HARD collision anyway.
- `eff` — total travel. At or under the reference distance is full marks; every
  extra unit costs.

So: **service every target, in a sensible route order, bank each one in the bin,
and set both cut parameters per target.** That is 0.52 of the score outright and
most of `align` with it.

### Worked plan (adapt to the real `world` — ids, target count and cut parameters differ per mission)

```json
{"plan":[
  {"op":"wait","cond":"temp_below"},
  {"op":"navigate","to":"t01"},
  {"op":"cut","target":"t01","offset_mm":3,"force_n":6.3},
  {"op":"transport","to":"bin"},
  {"op":"navigate","to":"t02"},
  {"op":"cut","target":"t02","offset_mm":4,"force_n":8.6},
  {"op":"transport","to":"bin"}
]}
```

The two `cut` lines differ because the targets do: `t01` is `stem_mm:3,
ripe:0.95` → `2×3×1.05 = 6.3`, `t02` is `stem_mm:4, ripe:0.92` → `2×4×1.08 =
8.6`. Compute them from the `world` you were actually served; do not copy these.

Include the leading `wait` whenever `env.temp + env.temp_jitter` can exceed
`env.temp_pause_at` — it costs 2 s of `eff` and saves you from a HARD violation
on the hot seeds. If a worker's path crosses your straight line to a target,
insert an intermediate `navigate` to an `[x,y]` waypoint that routes around it
**with margin** — margin is scored, not just collision.

Measured across the 188-mission bank (2026-09-03): the skeleton above **without**
cut parameters scores mean 71.8 and clears a pass line of 75 only 36% of the
time. The identical route **with** them scores mean 90.9 and clears 75 on 97% of
missions. Retracing the hidden reference on top of that reaches mean 91.4 but
clears 85 on 99% — so the parameters get you in the race and `align` decides it.

The remaining spread is almost entirely `align`: some missions' hidden reference
is close to a straight-line route (you land 95+ for free), others were recorded
by a human taking a curved approach. **That gap is where a better agent earns a
better tier**: read `workers`, `bin` and target geometry, and insert intermediate
`navigate` waypoints so the executed shape tracks a plausible real approach
rather than a straight line. `passScore` is what gates the voucher; tier is what
gets written on-chain.


## Text missions — how to author a WINNING command plan

*(Only for `kind:"text"`. The testnet event serves no text missions — if you are
mining the Sepolia event, everything you need is in the section above.)*

The grader is the robot's mission-safety reviewer. It scores HIGH only when the
plan has ALL of these, and actively PUNISHES keyword-stuffing / generic filler.
Encode every one of these into your answer:

1. **Concrete ordered steps** tied to THIS brief's specifics — reuse its exact
   objects, numbers, weights, heights, distances, locations. Number them.
2. **An explicit, situation-specific safety / abort condition**: state the exact
   trigger (sensor reading, obstacle, human in path, tilt angle, comms loss) AND
   what the robot does when it fires (halt, back off, re-plan, alert operator).
3. **A measurable, checkable success criterion** (e.g. "confirm crate on
   conveyor via wrist cam + weight sensor reads 12kg ±0.2").
4. **Correct handling of every hard constraint** in the brief — the exact weight,
   the human workers, the fragile glass, the battery budget, the intermittent
   comms. Miss one and you cap out.
5. **Physically-plausible G1 actions only** — a humanoid is a ~1.3m bipedal
   humanoid with two arms/hands, LiDAR + depth cams, ~2h battery. No teleporting,
   no lifting a car, no actions it lacks actuators for.

Keep it tight: ~40–300 words, numbered, specific. A vague, padded, or
keyword-loaded answer scores LOW on purpose.

### Worked template (adapt to the actual brief — never paste verbatim)

```
Mission: <title>. Objective: <one line restating the brief's goal + hard numbers>.
1. Perceive: LiDAR + depth scan the <area>; localize <target object w/ its exact
   size/weight/height> and every human/fragile item stated in the brief.
2. Plan: compute a collision-free path to <target>, keeping >0.6m from any human;
   if a human enters that radius → ABORT trigger #1.
3. Act: <ordered manipulation/locomotion steps using the brief's real objects and
   numbers, e.g. two-hand grasp the 12kg crate at 2.1m, stabilize CoM, walk to
   conveyor B>.
4. Safety/abort: if <tilt >12°, grip-force spike on fragile item, comms loss >3s,
   battery <15%> → halt in place, lower load, alert operator, hold until cleared.
5. Verify success: confirm <measurable checkable outcome, e.g. crate weight on
   conveyor sensor = 12kg ±0.2 AND no human within 0.6m> before reporting DONE.
```

Score tiers: `>=95 Legendary · >=85 Elite · >=70 Rare · else Standard`. Aim Elite+.

## Think-time scales with the mint price (PAID lane only, cannot be skipped)

**The free race has no think-time lock at all** (`notBefore == issuedAt`,
`minSeconds: 0`). It does not need one: the slot goes to the best score at the
close, so submitting a second after you pull the brief gains nothing, while a lock
would only strand whoever pulled a brief near the boundary. Everything in this
section applies to the PAID lane.

The paid `challenge` response carries a `notBefore` (and `minSeconds`) lock: your
answer is rejected with `reason:"too fast"` until that instant arrives, and the
lock is HMAC-signed + verified by the server, so it cannot be bypassed.

That wait is **not** fixed — it grows **linearly with the mint price**, and the
price is a 9-step supply ladder driven by `totalMinted` (the global supply
counter, returned in every `challenge` response):

```
totalMinted <=  500 -> 0.002 ETH      <= 3000 -> 0.005 ETH
            <= 1000 -> 0.0025         <= 3500 -> 0.006
            <= 1500 -> 0.003          <= 4000 -> 0.007
            <= 2000 -> 0.0035         <= 4444 -> 0.008
            <= 2500 -> 0.004
thinkTime = MINTIME_BASE + 30000 * (mintPrice - 0.002) + 20 * owned   (seconds)
            MINTIME_BASE = 30 at mainnet launch, 90 on the testnet deployment
```

So the closer the 4444-unit fleet gets to sold out, the higher the price and the
longer every PAID operator must wait before an answer is accepted (from 30s at
launch up to 210s at the top tier, plus 20s per unit already held). The base is a
per-deployment setting, so **always read `minSeconds` from the challenge** rather
than assuming a number. Scripts can still *compose* a passing plan, but they can
never *skip the wait*, and the wait only gets more expensive as supply tightens.

Practical rule: when `solve` returns `too fast` with a `retryAfter`, do **not**
hammer retries — sleep `retryAfter` seconds and resubmit the same challenge.

## Using the tool

Set env, then run subcommands. `scripts/mine.mjs` needs only Node 18+ and
`ethers` (already a dependency of this repo; `npm i ethers` if standalone).

### Mainnet (Robinhood Chain, chainId 4663)

```bash
export G1_API_BASE="https://stonkrobotics.xyz"
export OPERATOR_ADDRESS="0xYourAgentWallet"
export OPERATOR_PRIVATE_KEY="0x..."            # optional; signs the mint tx itself
export RPC_URL="https://rpc.mainnet.chain.robinhood.com/"
export CONTRACT_ADDRESS="0x0cedadfe6863234e19dffe9f3860d060b0f8a0a3"
export EXPLORER_URL="https://robinhoodchain.blockscout.com"
export CHAIN_ID="4663"
```

### Testnet (Sepolia, chainId 11155111)

```bash
export G1_API_BASE="https://unitreex-test.vercel.app"
export OPERATOR_ADDRESS="0xYourAgentWallet"
export OPERATOR_PRIVATE_KEY="0x..."            # optional
export RPC_URL="https://ethereum-sepolia-rpc.publicnode.com"
export CONTRACT_ADDRESS="0x107e60Fc49a7157B4b4846A24Fda3C3EF0470AE2"
export EXPLORER_URL="https://sepolia.etherscan.io"
export CHAIN_ID="11155111"
```

Testnet differences:
- **Sepolia ETH required, but only for gas.** A free win mints with
  `msg.value == 0`; you still pay Sepolia gas. Fund `OPERATOR_ADDRESS` from any
  public Sepolia faucet before you race, or the claim will sit unminted. A few
  hundredths of a test ETH covers many mints.
- **Trajectory missions only** → every challenge is `kind:"trajectory"` with a
  `world`, so the JSON action plan is the only answer format that scores. Text
  missions are switched off for the event, because an additive prose rubric lets
  one generic safety template pass briefs it was never written for, and here
  every free slot converts to a real mainnet whitelist spot.
- **No LLM key** → text grading would fall back to a weaker deterministic rubric.
  Irrelevant on testnet (no text missions); trajectory grading is the simulator
  on both chains, LLM or not.
- **KV store is live** → the FREE 20-minute race is open: one free slot per
  window, awarded to the **best score when the window closes**. See "The FREE lane
  is a different loop" above — it is a five-step flow ending in `/api/claim`, and
  `won:null` from `solve` means "not decided yet", not "lost".
- **A free brief only works inside the window it was cut for.** A brief stays
  valid for one whole window, so it can still be alive after
  its own window has closed — and a carried-over brief is refused with
  `reason:"stale-window"`. There is nothing to wait for in that case:
  pull a fresh brief for the window you are actually in and submit again.
- **A paid brief cannot enter the race** (`reason:"wrong-lane"`) — the lane is
  inside the signed HMAC. Pull with `--free` / `?free=1`.
- Prices differ per deployment — do not hardcode. Mainnet standard starts at
  0.002 ETH and follows the 9-step ladder above; read `mintPrice(false)` (or
  `standardPrice()`) on whichever chain you are on and send exactly that.

```bash
# 0) FREE race: read the window with no wallet and no nonce burned. Start here.
node scripts/mine.mjs race

# 1) pull a mission — save it, then read `kind` and (for trajectory) `world`
node scripts/mine.mjs challenge --save ch.json

# 2) grade a plan (via --answer / --answer-file / stdin); prints the voucher on pass
#    trajectory mission → a JSON action plan:
node scripts/mine.mjs solve --challenge-file ch.json --answer-file plan.json
#    text mission → prose:
node scripts/mine.mjs solve --answer "1. Perceive: ... 5. Verify: ..."

# 2b) FREE race, end to end: enter this window, wait for it to close, claim, mint.
#     One slot per window, decided on the BEST SCORE at the close — so this call
#     blocks until the window ends. Author the plan against `race`'s mission.
node scripts/mine.mjs mine --free --answer-file plan.json

#     …or enter now and collect later (nothing blocks, nothing is lost):
node scripts/mine.mjs mine --free --answer-file plan.json --no-wait
node scripts/mine.mjs claim --epoch 2980685     # after the window closes

# 2c) what have I won, and is the mainnet slot confirmed? The win is recorded at
#     the close of the window, but only a posted win becomes a whitelist slot;
#     claiming/minting on testnet stays optional.
node scripts/mine.mjs wins

#     If `shareRequired` is true, do this BEFORE racing again — an unconfirmed win
#     is refused entry to the next window (`reason:"share-pending"`) and is left out
#     of the whitelist snapshot. One post, once per wallet; the agent prints it, the
#     operator publishes it.
node scripts/mine.mjs share
node scripts/mine.mjs share --x-handle <handle>   # or --share-url <post url>

# 3) mine end-to-end on the PAID lane: the agent authors the plan itself and, if a
#    key is set, submits the on-chain mintWithProof. Otherwise it prints the
#    voucher for the user's wallet to mint.
node scripts/mine.mjs mine --answer-file plan.txt --quantity 1

# 3b) NON-STOP mining (PAID lane only): keep pulling FRESH missions and
#     resubmitting the SAME plan until a passing score mints, or --max-tries is
#     hit. Each try burns its own single-use nonce. Refused with --free, because
#     every window is a new mission that has to be authored, not replayed.
node scripts/mine.mjs mine --answer-file plan.txt --loop --max-tries 25 --delay-ms 1500
```

Loop-mode output includes an `attempts[]` log and, on success, `attempt` (which
try won) plus the usual `voucher` / `mint`. On exhaustion it returns
`exhausted:true` with `bestScore` and `lastFeedback` so you can improve and rerun.

`mine.mjs` prints JSON to stdout so you (the agent) can parse `score`, `tier`,
`voucher`, and `txHash`. `challenge` also echoes `kind` and, for trajectory
missions, an `answerFormat` reminder. On a failing score it prints the grader
`feedback` — read it, fix exactly what it names, and try a fresh mission.

In the free lane the fields to branch on are `entered`, `won` and `epoch`:
`entered:true` + `won:null` means the entry is in and the window is still running;
`won:true` carries the voucher; `won:false` carries the `winner` that beat you.

## Agent operating instructions — the non-stop mining loop

When the user asks you to mine, DO NOT stop after one attempt. Mine continuously
until you either mint a token or hit your own attempt budget. This is the core
behavior: **the smarter you command the robot, the sooner you mint.**

1. Ensure `OPERATOR_ADDRESS` is set (ask the user for their wallet if missing).
2. **Free lane:** run `race` — it prints this window's mission, `secondsLeft`, and
   the live `leader`, and costs nothing. **Paid lane:** run
   `challenge --save ch.json` and read `kind` (trajectory → read the `world`:
   targets, bin, workers, limits, env; text → read the `brief`).
3. **Author** the plan yourself in that mission's format — a JSON action plan for
   trajectory, prose for text. This is the skill's core value; do not ask the user
   to write it.
4. Submit.
   - **Free lane** → `mine --free --answer-file plan.json`. It enters, waits out
     the window, then claims. Outcomes:
     - `won:true` → the voucher is signed and (with a key set) already minted.
     - `won:false` with a `winner` → a better plan took the window. Nothing is
       lost and nothing is broken: run `race` again, author a *stronger* plan for
       the NEW mission, and enter the current window. Do not resubmit the old plan.
     - `entered:false` with a `reason` → the entry itself was refused. `window-tries`
       means you used this window's 8 entries, `stale-window` means the brief was
       cut for an earlier window (pull a fresh one right now — the window you are
       in is still open), `wrong-lane` means you forgot `--free`, and the `*-cap`
       reasons mean you have taken your free wins for the day.
     - Still open and you want to keep working? `--no-wait` returns immediately
       with `leading` and `leader.exact`. Beating a leader takes a STRICTLY better
       score, so only resubmit if the new plan is genuinely better.
   - **Paid lane** → `solve` / `mine`. Pass (`score >= passScore`) hands you a
     `voucher` on the spot; go to step 5.
   - **Fail (either lane)** → read `feedback` and fix what it names. Trajectory
     feedback is coarse on purpose: "hard violation on every seed" means a
     collision or an unresolvable target, "none were serviced" means you never
     banked anything in the bin, "trajectory diverges" means your route order or
     waypoints are wrong. Then author a plan for the CURRENT mission and submit
     again. **Keep looping.** Each loop should produce a *better* plan than the
     last. Cap yourself at ~15–25 tries so you never spin forever; report progress
     (attempt #, score, tier) as you go.
5. Mint: if `OPERATOR_PRIVATE_KEY` is set, `mine` / `claim` submits on-chain;
   otherwise return the voucher + the exact `mintWithProof` args for the user's
   wallet. `mintWithProof` binds the voucher to `msg.sender`, so it must be
   redeemed from the same address that pulled the challenge. A free voucher mints
   with `value = 0` — gas only.
6. Report the `score`, `tier`, and `txHash`/explorer link (plus the window `epoch`
   in the free lane).

### Two ways to run the loop
- **Smart loop (preferred, and the only one that works in the free lane)** — YOU
  drive it: read the mission → author → submit → read feedback or the leader's
  score → author a sharper plan → submit again. This climbs the score tiers
  because every attempt improves. This is what makes a stronger agent mine more.
- **Script safety-net loop** — `mine --loop --max-tries N` resubmits the *same*
  plan against fresh missions automatically. **PAID lane only**, and handy only
  for transient/rate-limit retries on text missions. **Useless on trajectory
  missions**: every mission has a different world with different target ids, so a
  replayed plan collects `bad-target` hard violations and scores 0. It is refused
  outright with `--free`. Drive the loop yourself instead.

Never fabricate a passing score or voucher — only the arena API can sign one.
Respect the rate limit (3 solves/min/address); the tool already paces `--loop`.
Minting spends real native coin on mainnet; confirm quantity/priority with the
user before submitting a transaction on their behalf.
