FutureX
FutureX is a live benchmark for agents that predict future
events. Each week it publishes a batch of open questions on Hugging Face; entrants answer before a
deadline and are scored once the events resolve. Marina enters with its ordinary forecasting
engine — the same typed forecasts any user gets. The adapter in
benchmarks/futurex/ and scripts/futurex.ts only translates the batch into general forecast
calls and writes the submission file. Nothing in Marina’s core knows about FutureX.
Commands
Section titled “Commands”bun run futurex fetch # the current batch, at its Hugging Face commit shabun run futurex fetch --past # the resolved-questions dataset (for backtests)bun run futurex run # forecast every row with the earned live configurationbun run futurex run --variant cheap # a fixed configuration instead (the override for experiments)bun run futurex run --variant cheap --variant frontier # several variants, one file eachbun run futurex backtest --limit 40 # resolved rows: forecast with an early cutoff, score, recordbun run futurex backtest --clean --isolation closed-book --variant cheap --limit 160 --replicates 2 --lessons on # a non-leaking backtest (see Clean backtests)bun run futurex watch --once --run earned # poll for a new batch; on one, fetch and runbun run futurex watch --once --run earned --daily --learn # …and re-forecast open rows daily (see Standing answers)bun run futurex watch --once --run earned --daily --daily-rows stale --near-end-hours 48 # …daily runs touch only stale rows (see Standing answers)bun run futurex watch --once --run earned --answer late --final-lead-hours 6 # …answer each row once, close to its cutoff (see Answering late)bun run futurex schedule --run earned # PRINT a systemd service + hourly timer for the late watcherbun run futurex learn # resolved rows of filed batches → outcome lessonsbun run futurex status # every submission recorded so farFiles go to data/futurex/ (ignored by git; --dir to change). Benchmark questions are never
committed. DB_PATH chooses the ledger (default marina.db). The forecaster’s own settings
(MARINA_FORECAST_*, forecasting) apply; OPENROUTER_API_KEY is
required and TAVILY_API_KEY enables date-filtered search.
How a row becomes a forecast
Section titled “How a row becomes a forecast”benchmarks/futurex/map.ts reads the answer shape each row asks for:
| The row | The forecast |
|---|---|
lettered options (A. …), or boxed alternatives in older rows |
choice — or multi for a level-2 bundle of independent outcomes (nominations, who qualifies, cumulative thresholds such as “at least …”); a count with bucketed options (“how many …”) is always one choice |
| “how many …”, a unit (“Report the value in …”, “…, in knots, …”), a measured value, or an official statistic titled by its series and period (“Job openings — July 2026”) | number (an integer for counts; the unit is passed on) |
| one top entry (“which club will be first …”, “rank No. 1”) | text |
| an ordered or “which N” list, “ranked from X to Y”, “which |
ranking, sized when the row says how many |
| anything else (an exact title) | text |
Each rule is checked against the shape of every resolved row’s truth. test/forecast-shape-scale-selection.test.ts holds synthetic cases. With FUTUREX_SHAPE_AUDIT_BATCH=<past batch json> set, it also checks, by row id, the rows a failure analysis found mistyped.
The full prompt goes along as the forecast’s context (it carries the settlement rules), and the
row’s end time becomes the evidence cutoff. The prompt’s own format line (IMPORTANT: End with … \boxed{…}) is stripped by default. --format-line keep (or "formatLine": "keep" in a variant)
keeps it, so the general answer contract can read it when
MARINA_FORECAST_CONTRACT is observe or on. The default is unchanged, so the line’s effect can
be measured as one switch; the choice is part of the variant, so a run’s journal and ledger
configuration record it.
Freezing and filing
Section titled “Freezing and filing”- Every answer uses nothing published after the earlier of now and its question’s end time. A row
whose end time has already passed is answered with its cutoff at that end time and flagged
late. - Every row gets an answer: a missing prediction scores zero, so when no run produced a usable
answer, the runs’ best partial answer (or, for a choice, the first substantive option) is filed
and flagged
fallback. - The submission file is named
org-<org>-agent-<agent>-model-<model>.jsonand holds[{"id", "prediction"}]. Marina files as its own agent,Marina, under the organizationh2o.ai(--org,--agent); the model segment names the variant’s model (--modeloverrides it for a single variant). - Nothing is sent.
runprints the file path and the email fields (recipient, subject, dataset commit, model, framework, organization). Submitting is an operator’s act, or an approved connector’s.
Standing answers
Section titled “Standing answers”FutureX questions stay open for days, so a filed answer is a standing answer per row and variant
(out/<sha>/<variant>/standing.json). watch --daily re-forecasts the rows still open:
-
Cadence: once per UTC day at or after
--daily-hour(default 06:00), plus one final run--final-lead-hours(default 4) before the batch’s deadline. Nothing runs after the deadline, and only the batch that is still the latest is re-forecast: a newer batch ends the older one’s re-forecasts. -
Deadline: the first Wednesday 16:00 UTC after the batch was published (the Hugging Face commit time of its sha, recorded as
publishedAtwhen the batch is fetched), never later than its last row’s end time. It never depends on when this machine fetched the batch, so a batch first seen after its deadline gets no daily runs. A batch saved beforepublishedAtwas recorded falls back to its first fetch time (benchmarks/futurex/schedule.ts). -
Rows: the first run of a new batch forecasts every row. A daily run re-forecasts open rows only: with
--daily-rows all(the default) every open row; with--daily-rows staleonly open rows whose standing answer (per variant) is missing or a fallback, or whose end time is within--near-end-hours(default 48) of now. The final run re-forecasts every open row. -
Schedule: each run is written to
out/<sha>/schedule.jsonwhen it starts (status: "running", with the planned rows per variant) and markeddoneorfailedwhen it ends. A check while a run is in progress starts nothing. A run that never recorded its end is treated as in progress for--in-progress-hours(default 6); after that it is abandoned and counts as that day’s run. -
Revision rule (
src/forecast/revision.ts): a new answer replaces the standing one only on a material change:- its confidence rises by at least 0.1; or
- its evidence is materially new (word overlap below 0.6) at about the same confidence.
An unchanged answer (ignoring case, spacing and list order), a number within 2 %, a fallback, or a different answer with nothing behind it keeps the standing answer.
-
Ledger: every decision, kept or revised, with its reason, is appended to
revisions.jsonl. A run that revises something writes a new file, which is a newexternal_submissionsrow, and prints the email fields again. A run that revises nothing reproduces the same file, which is already recorded, and prints nothing to send.
Answering late and scheduling
Section titled “Answering late and scheduling”watch --answer late does not forecast a batch when it arrives. Each open row is answered
once, inside --final-lead-hours of its own cutoff. The cutoff is the batch deadline, or the
row’s end time when that comes first. So the answer sees as much news as possible and costs one
forecast per row (lateRows in benchmarks/futurex/schedule.ts). A row whose answer is missing or a
fallback is retried on the next check inside its window. Nothing runs at or after the deadline,
--daily is ignored, and the in-progress rule and schedule.json work as above. watch itself
still defaults to --answer early.
Nothing schedules the watcher for you. bun run futurex schedule prints a systemd user service and
an hourly timer that run watch --once, answering late by default, with a 6 h lead. Add `–out
bun run futurex schedule --run earned --dir ~/futurex-data --env-file .env --scope-cap 25 --out /tmp/unitscp /tmp/units/marina-futurex-late.* ~/.config/systemd/user/ && systemctl --user daemon-reloadsystemctl --user enable --now marina-futurex-late.timer # only when you mean itOptions: --answer early|late, --final-lead-hours N (default 6 here), --run <variants> (default earned),
--variants <file.json>, --dir, --learn, --repo (working directory, default the current one),
--bun (default the running Bun), --env-file, --db-path (sets DB_PATH) and --name (unit
name, default marina-futurex-late). Every model call is charged to the unit’s own spend scope
(MARINA_SPEND_SCOPE=<name>), and --scope-cap (USD, default 25) must be above 0, because an
uncapped timer is refused. The timer is Persistent=true, so a check missed while the machine
slept runs on wake.
--learn (or bun run futurex learn) hands resolved weeks to the
outcome-lesson loop on every watch tick,
whether or not a re-forecast is due. Each standing answer whose row has a ground truth in the past
dataset is scored and recorded once per variant and row (learned.json), so a repeated pass adds
nothing; the past dataset is re-read only when its commit changed.
The question, truth and answer are seen only by the lesson writer as private context, never stored.
A row’s outcome counts as known one day after its end time (SETTLEMENT_MARGIN_MS in
benchmarks/futurex/lessons.ts), never at the end time itself: the rows of a week share one end
time, and a replay’s cutoff is that end time, so a sibling’s lesson can never reach it.
Records
Section titled “Records”- Each submission is appended to
external_submissions(migration 152): batch sha, variant, identity, file name and hash, rows answered, cost. The same file is recorded once. - A backtest — and a live batch once it resolves — is a scored run in the
benchmark ledger, so
benchmark compare,frontier,leaderboardandreplicatesrank it with every other run Marina has made.
Variants
Section titled “Variants”Built-ins are:
earned(the live default forrun,watch --run earnedandschedule): no fixed configuration. Each row goes through Marina’s forecast surface (typedForecastFor) on boardfuturex: the operator’s env, then the earnedforecast-config:futurexslot, then the family slot, then the installation’s built-in (resolveDefault), plus card routing and the lesson pool read as a live run. FutureX’s own pages stay barred from research. The run log prints where each field came from. A slot changes only through earned promotion (fileSlotPromotion, e.g.rescore-selection.ts --file).cheap: one strong low-cost model, three runs. The backtest default: backtests stay fixed controls and never takeearned.verify: cheap runs with every draft checked by a different-vendor verifier.frontier: three vendors’ current models, three runs, a frontier critic.frontier-2607: a frontier mix released before the latest resolved rows, so it can be backtested cleanly.crew: a Marina crew as the analyst, viamarina:<crew>.
Model ids are pinned, never floating aliases. --variants <file.json> supplies others: an array of
{ label, model, analysts, planner?, critic?, verifier?, verify?, runs?, researchRounds?, critique?, selection?, budgetMs?, groundingFollowUp?, formatLine? }.
selection: "confidence" takes each row’s most self-confident run (see Forecasting). budgetMs is a per-row time budget: the answer is combined from what has finished, and is labelled budget_forced in the ledger.
groundingFollowUp bounds the forecaster’s follow-up when no run is grounded in verified evidence.
With "all" (the default) one more research round runs, and every run is redone if that round
verified something new. With "one" a single fresh run reads the fuller dossier and replaces the
least-grounded run before the runs combine. With "off" the round is skipped. When the trigger
fires, the answer records the mode, whether the round ran and how many runs were redone in its
groundingFollowUp field.
bun benchmarks/futurex/rescore-selection.ts --dataset <past batch json> --runs <answers.json>,… re-scores saved runs under each selection mode, with no model calls. Rows split into selection and holdout halves by a stable hash. It reports whether confidence clears the promotion margin over agreement on the holdout half; pass --tried with the number of candidates already examined. Its own verdict is advisory. To change the default, add --file --variant <name> (the configuration the saved answers ran with; --db defaults to DB_PATH). This records one ledger run per answers file per mode and files the challenger through the one promotion path on forecast-config:futurex, the slot live FutureX reads. An empty slot is first seeded with the incumbent, so a challenger never takes an empty slot uncontested. The printed verdict is the promotion path’s: replicate minimum, holdout interval, fishing margin, and an append-only history row.
Live runs recall from the one judged lesson pool (forecast and arena domains, the same
forecastLessonsFor every forecasting surface uses; MARINA_LESSONS on / observe / off);
--lessons off disables that.
Lessons kept by the earlier, unjudged store (subject forecast-lesson in the forecast-lessons
space of a Forecaster world account) are copied into the pool once — by futurex run and
futurex learn, or explicitly with bun run futurex migrate-lessons — as unverified lessons with
their original resolution time and a provenance record (store, account, space, id, version). Each
copy is keyed by the record’s id and version, so a re-run never writes it twice. The originals are
never changed; when every record has been copied and the account was created by the script, it is
renamed to marina:legacy-forecast-lessons, a server-owned name no login can produce.
Clean backtests
Section titled “Clean backtests”bun run futurex backtest --clean measures skill on resolved rows without letting outcomes in.
Each of the three ways an outcome can leak has a guard (benchmarks/futurex/clean.ts):
- Model weights. Only rows whose cutoff (the end time minus
--horizon-days, default 7) is more than 3 days (KNOWLEDGE_MARGIN_DAYS) after every model’s public release are used — by default, rows that end more than 10 days after it. This is the one rule configuration selection uses too (benchmarks/forecasting/knowledge.ts: one release table, one margin, floating aliases refused everywhere). Release dates come from the provider catalogue (MODEL_RELEASES) and are an upper bound on knowledge cutoffs. A model with no known release, a floating alias, or a crew (whose agents have their own tools) is refused unless--after <YYYY-MM-DD>is given. Every variant in one invocation runs on the same rows. - Retrieval.
--isolation date-filtered | post-filtered | closed-book(with--retriever <spec>for the first two). An unfiltered engine is never used. - Memory.
--lessons onwrites a lesson after each scored row and recalls only lessons visible at each forecast’s cutoff. Rows run in order of resolution. Each run recalls only its own lessons, so--lessons onagainstoffis an honest ablation, and every lesson is also kept in the shared space for live runs.
Leak audit. After scoring, each row is audited. A row is suspicious if its reasoning names a day after the event, it quotes the exact numeric outcome, or its kept evidence includes a page published after the cutoff. An evidence line dated on or before the cutoff counts as history and schedule, so past-tense wording and future dates in it are not flags. Undated evidence is suspicious if it names a day after the cutoff or reports a result. Headline scores are given with suspicious rows in and out.
Upper bracket. --allow-contaminated --isolation contaminated runs an unfiltered engine on
past cutoffs. It can see outcomes, so it is filed under futurex-past-contaminated as an upper
bound, never as a clean score. Between it and closed-book lies the range a live run can fall in.
Judged scoring. --judge <model> also grades string and list answers with a model judge, as
the official weekly scoring does (mechanical string matching under-credits paraphrases). Options
and numbers stay mechanical, and both overalls are reported.
Results. Each run gets the overall and per-level scores with a bootstrap interval, plus a score
per batch week compared with --reference <file.json> (week → top, median, h2o, entered by
hand; the website is never scraped). --replicates N repeats a run. Every run is filed into the
benchmark ledger under a replicate group, futurex-clean:<variant>:<isolation>:lessons-<on|off>.
A run in which more than MARINA_BENCHMARK_MAX_FALLBACK_RATE (default 25 %) of the rows got a
fallback instead of an answer is recorded invalid and drops out of pooling and comparison; retire
or restore a run by hand with benchmark invalidate|revalidate <run> reason:<text> or
bun run benchmark:import --invalidate <run> --reason "<why>".
--first-replicate N numbers new replicates from N, so a later run adds replicates without reusing
an earlier run’s name or lesson space.
Spend cap. Every futurex command that calls a model records its spend in the world
database’s daily ledger (DB_PATH), shared with the server and every other run on that database,
so an hourly timer does not get a fresh cap each run. Give each job its own budget inside the world
cap with MARINA_SPEND_SCOPE=<name> and MARINA_SPEND_SCOPE_CAP_USD=<usd>; a run is refused at
either cap. A run stops starting new rows when its spend plus a reserve (1.5 × concurrency × the
average row cost, at least $2) would reach the tighter of the two caps. A run stopped this way is
not filed. Rows that fall back because the cap or a provider refused them write no lessons.
Partial runs and --resume. Every finished row is appended to rows.jsonl in the run’s output
directory as soon as it lands. A run stopped by a cap keeps those rows: a clean backtest writes
partial.json beside them (reason, rows finished, cost) and files nothing to the ledger. Rerun the
same command with --resume to forecast only the rows still missing; fallback rows run again. A
journal written under another configuration (variant, rows, horizon, batch, isolation, retriever,
lessons, replicate) is refused; run without --resume to start over. metaculus select and
forecastbench select keep the same journals per candidate and replicate (<selection>-runs/); a
resumed selection files each run once and counts only new spend against the selection budget.
Backtests without --clean are smoke tests
Section titled “Backtests without --clean are smoke tests”The resolved dataset’s outcomes are public. A backtest moves each cutoff --horizon-days before
the question’s end time (default 7) and searches with date filters where the engine supports them,
but web search without a date filter can still surface the answer. Treat a backtest as a check
that the pipeline works end to end, never as an estimate of live skill. The local scorer
(benchmarks/futurex/score.ts) follows the published metric definitions — exact match or F1 for
options, a squared error scaled by 5 % of the true value for numbers, exact or partial overlap for
lists, level weights 10/20/30/40 % — and matches strings mechanically, where the official scoring
uses a model judge.
