Building a maths agent
Delegating the queen of the sciences to the machines
2026-05-31 — 2026-08-15
In Which an Iterative Harness for Mathematical Reasoning Is Constructed, Wherein the Significance of a Robust Execution Loop and Scratchpad Memory Is Weighed Against the Simple Cost of Model Fan-Out.
This page is mostly AI slop, i.e. LLM descriptions of my decisions in some recent exploratory projects. However, it is useful, so I am publishing it now rather than waiting for a more polished version.
Mathematical reasoning agents are a thing, and as a working mathematician, I want them. Or rather, if they are to exist, I don’t want to be the one without them. And if they do exist and I have one, I want one that lets me reason faster over new domains, check my work, and augment my feeble meat brain as smoothly as possible.
The theory and practice of how to do this are weirdly accessible, utterly not what I expected, and relatively simple to implement.
That last property is fortunate, as the marketplace is thin. As working mathematicians, we are resigned to using tools that predate modern conveniences. The field does not exactly seethe with polished, friendly software off-the-shelf, as it would if we were doing things more central to the profit chain, such as munging office memos, or for agentic coding.
So — we build our own, I guess? I have been begrudgingly building my own as a learning exercise. This post is distilled from my attempt to speed-run the last two years of mathematical reasoning LLM advancements for my own benefit.
I wanted it to be a cogent and principled introduction to how to implement and optimize in practice for the mathematical skill that we believe LLMs to have. In practice it is more of a stream-of-consciousness lab notebook put through the AI blender. tl;dr good mathematical reasoning ends up being somewhat about models and much more about specialized harness design, with the design criteria determined by whether we want answers, or proofs or formal proofs. The first is solver, the second is kinda the default if you use an LLM and the third requires special skills in an obscure language, Lean.
I already run consumer-grade agent harnesses for code — a model and a sandbox and a loop that splices the two. What do I need to add to get sweet theorems out of those?
1 The path through
Suppose I already use some coding harness. I can proceed step by step to add features, one at a time, walking through harness design space until it is more optimized for mathematics than for code. That space has four axes — wiring (how the calls connect to one another), verification (what gets to say an attempt was any good), context (what each call is shown) and dispatch (which model or tool fills which slot) — and every step below moves exactly one of them, which is what the second column of the table records. Some steps, in a recommended order (I won’t divulge whether I actually followed this order):
| Step | Axis moved | What it buys | Cost to build | Measured effect |
|---|---|---|---|---|
| Easy mode | — | an agent with a generalist backend, used cleverly | free | the baseline |
| Lend it an oracle | dispatch | a maths model behind a solve() tool, for the sub-problems the generalist should not attempt |
an afternoon | least of anything on offer |
| Build the loop | wiring → sequential | an explicit generate-run-verify system, specialist models throughout if we want | a weekend | largest single contributor |
| A scratchpad | context | what failed carried across attempts, so the loop stops repeating itself | hours, once the loop exists | second largest |
| Fan-out | wiring → parallel | many samples, cross-checked — compute traded for accuracy | cheap to build, dear to run | large, but only as good as whatever picks the winner |
| Lean | verification → exact | the problem formalized into machine-checkable form | days of toolchain plumbing | changes what every other step can do |
Maths, as an automatically-verifiable problem class, is amenable to all kinds of nice tricks. A maths executor by default only ever checks a calculation, which is side-effect-free. As such, fifty attempts will not clobber each other, so we can, e.g. fan-out and try everything everywhere all at once.
These all cost different amounts of effort to build, and yield different amounts of effect. That last column is mostly not my measurement. The loop, scratchpad and fan-out rows come from AxProverBase (Requena et al. 2026), a deliberately stripped-down Lean-proving agent whose authors assembled it one component at a time and scored each addition separately — the nearest thing to a controlled experiment on any of this that I know of. Ranked by effect, they put iterative refinement first and memory second, which are the third and fourth steps here; building the oracle is nevertheless easier and more fun than either. The remaining two rows are mine. The oracle row reads across from their sweep over frontier models rather than from an ablation of anything oracle-shaped, and the Lean row is not a measurement at all — the compiler is the substrate their agent runs on, not a component they could take out and score.
I suspect that the table continues past my ability to build: the recursive, self-grading, re-formalizing orchestrators at the top of the mathematics leaderboard have enough startup money behind them that I am not going to reproduce one in a weekend. That is not an argument for buying instead, though, and the numbers do not make one either. The home-built loop comes out around five times cheaper per problem and solves about half as many: cheaper per attempt, weaker per problem, and the trade is ours to make rather than theirs.
Two maths-harness gotchas:
- TIR models speak their own tool-call dialect, so the orchestrator has to parse fenced code templates and splice results back in the shape each model expects. Budget for writing that parser per model family; it is the thing that stops a generic harness working.
- The model writes the code we run unobserved, so sandboxing is wise. The only maths-specific detail is that the specialists assume a full scientific Python (SymPy, NumPy), so build the sandbox accordingly.
2 Easy mode
The baseline.
We need not be galaxy-brained. Claude works for maths just as well as it does code, as do most generalist models — The latest DeepSeek, or a big Qwen3-Thinking are all respectable mathematicians; tell them to “do the maths”, then “check the working with Python/SymPy”.
NB local clients are less capable at rendering equations, so I find I want to build Open WebUI to make equations easier on human eyes, but this is definitely lily-gilding.
3 Lend it an oracle
Dispatch tricky bits to specialist models.
Now, if we want a little more juice, we can squeeze it from our existing agent, using it as an orchestrator but delegating mathematics (or the checking of mathematics) to a specialist maths model. Practically, this means something like building an oracle (a solve() tool) to grind on knotty maths problems, splicing the result back into the orchestrator’s own reasoning. Oracle is meant in a strict sense: the orchestrator does not attempt the sub-problem nor does it check the answer. Rather it asks and believes with whatever confidence it is instructed to have.
Python harnesses like Smolagents or Qwen-Agent can simply call a Python function which wraps structured invocation of specialist models. For other agents (Claude-like, Goose, Codex, Gemini CLI, pi, Hermes) we can expose the solve() as a CLI tool. Either way, we wrap it with an agentskills.io SKILL.md to help the calling agent. Skill-blind agents can alternatively use an MCP server.
I whipped up mathx as an example of doing all three access paths — importable Python, CLI, MCP server — with the SKILL.md layered over the first two, in a relatively swappable manner.
We might wish to fan-out in such an oracle. Done synchronously, this can be slow and hit time limits on tool use, or just get boring. Asynchrony and parallelism depend on support in the orchestrating agent. When we own the loop, a fan-out is as simple as asyncio.gather. For tool-use agents, the async-handle pattern is probably the goods: We set up a handle-and-poll loop in pair of ordinary tools: submit_solve() returns a job id at once and check_solve(id) returns status-or-result at once, so each call is fast. Some harnesses also dispatch background sub-agents of their own, and some MCP clients support Tasks at the protocol level; that might work too.
4 Build the loop
Sequentially iterate solutions, learning each time.
If easy mode plus an oracle aren’t enough, we can write a custom loop that specializes the generic one for mathematical needs.
What if we are trying to solve a hard problem? Intuitively, it makes sense to grind, learning from past mistakes at each attempt. Building a loop that supports this turns out to be kinda good actually. In the wild I have seen this as a two-role iterative system. A proposer writes a candidate answer or proof, and an executor runs or checks what the proposer wrote. The verdict — a compiler error, a failed numeric check — is fed into the next prompt as a minimal trace of what failed last time. Round it goes again until the check passes or the iteration budget runs out (AxProverBase allows 50 rounds per problem). This is not the same thing as fan-out: there the attempts are independent and mutually blind, whereas here attempt \(n+1\) has seen what attempt \(n\) got wrong. This is the single largest contributor to success increases in the AxProverBase ablations, and it is also where the maths usage diverges from the coding usage.
5 A scratchpad
Exploit context to record a longer narrative.
We can build more elaborate memory suited to an epic proof attempt, specifically, not just last turn, but of the whole proof journey thus far. The crude version pastes the whole transcript of past attempts into the prompt, but that bloats the context, and eventually overflows the window. The SOTA variant requires that after each attempt the proposer rewrites a running scratchpad of what it learned, pruned so it never grows without bound. In AxProverBase, that method buys around 7% more theorems at ~20% lower cost, and with lower variance than memoryless attempts and completist memory strategies.
6 Fan-out
Parallel proposals and checking.
Fan-out improves mathematical accuracy by evaluating many candidate solutions — maj@k or Pass@k, depending on our backend. We can run a lot of these fuckers at once, since the tokens stream over the network in parallel.
The goal here is overall efficiency: do more parallel copies of a cheaper model get us further than fewer copies of a more powerful one? For problems that are just on the cusp of being solvable, this turns out to be helpful.
Sampling wide is not too crazy. The hard bit is working out if any of the answers were good.
6.1 Picking a winner from answers
The classic idea is to vote on plain-language answers and pick the most common, which we call maj@k. Implementing it is not trivial, because deciding whether two answers are the same is a soft verification problem in its own right, and provably not a solvable one. This was a PITA when I implemented mathx.
Is 0.5 \log \cos(ax-b) the same as \frac12 \ln \cos(b-ax)? The standard fix (e.g. in math-verify) is to parse both into a computer algebra system and test equality there. Better than nothing, not amazing: the LaTeX inconsistencies are each fixable one patch at a time and the supply of them is inexhaustible, while the heuristic escape hatches (math-verify’s strict=False) are way too permissive, counting answers as the same because they happen to share a greek letter.
A fuzzy but interesting fallback is to use LLMs for this too, delegating to the decision about whether two things are the “same” to yet another model invocation OpenAI’s simple-evals, for example, scores MATH with a model-based equality-checker prompt rather than rules; NVIDIA’s NeMo-Skills ships both an LLM judge and sympy checks. There exist trained verifier models, but they are less useful than they sound: they grade candidate-against-ground-truth, not candidate-against-candidate, i.e. they don’t help with novel mathematics. xVerify has bad licensing, and the Omni-MATH-2 audit (Ballon et al. 2026) found Omni-Judge wrong in 96% of its disagreements. CompassVerifier (Apache-2.0) is a credible trained option, and its human-labelled VerifierBench is a free test set for verifying judge models in general. Clearly though, we might get worried about diminishing returns of endlessly layering LLMs on LLMs to patch imperfections in LLMs.
In the little toy mathx project, I went for a tiered, audited approach so I can experience all the failure modes so I can swiss-cheese my problems. Exact match wins, then CAS equality, then an opt-in LLM judge for any leftovers from the CAS. In all cases, we check both orders (\(x=y\) and \(y=x\)) and reject anything inconsistent or unparseable. We disclose the methods used in each case. (e.g. 11/12 (3 judge merges)). On one of my test problems (maj@16, of which 12 samples were parseable), CAS fixes got us from 4/12 naïve matches to 8/12, and an LLM judge up to 11/12.
6.2 Picking a winner from proofs
Everything so far was about answers (what is \(7^{2026} \bmod 13\)?). Hunting proofs instead (prove that \(7^{2026} \bmod 13 = 4\)) removes the option of counting: absent a formalization, the aggregation step can only be a judge that reads them. I have not built anything so elevated.
Possibly helpful for this: OpenRouter’s Fusion Router, a pre-rolled, hosted fan-out-and-judge which looks like it might handle this kind of thing. Point a request at openrouter/fusion: a panel of models answers in parallel, and a judge model compares their answers to produce structured consensus/contradiction/blind-spot analysis that feeds a final answer. This seems fine.
7 Lean
Moves verification to an exact grade: the checker cannot be talked round.
So far we have been imagining english-language mathematical reasoning, which has ended up being kinda painful. Whacking in a proof assistant (in practice, always Lean) makes this much cleaner, at the cost of being weirder. Modal’s theorem-proving case study shows how it is done.
Here, the model emits candidate proofs in Lean, they are submitted to the Lean language server, and we get back {"ok": bool, "errors": [...]}, which tells us whether the proof landed.
A Lean toolchain can be tedious to set up, including gigabytes of “Mathlib” that takes hours to build and comprise brittle, version-sensitive plumbing. Various Lean technologies support this workflow (lean-repl (the lightest), Pantograph, or LeanDojo, usually behind the Kimina Lean Server to keep it warm).
The weirdness now, that is in the very manner we express mathematics. Lean is a formal language, guaranteeing formal proofs of formal problem statements, so obviously it needs formalization if we start with plain language problem statements — ideally autoformalization. One-shot autoformalization models (Kimina-Autoformalizer, Goedel-Formalizer-V2) suck though. This formal proving thing seems to work fine for formalized proofs, but getting the problem into such a state is unreliable and difficult to debug.
7.1 Lea
Lea (source/my fork) is a Lean 4 agent backbone from NYU’s VIDA lab, with a standalone web client and a Chrome extension.
Since the whole thing is carved out by the NYU folks it all just works. docker compose pull && docker compose up fetches a docker image with Lean and Mathlib already inside — about 3.7 GB down, 10.7 GB on disk. We can forget all this version pinning bullshit, as long as you are happy to live with VIDA’s preferred version of everything.
It interactively and iteratively formalizes labelled theorem blocks straight out of a locally-hosted Overleaf document, which is wayyyyy more civilised than rawdogging Lean for a n00b like me. All the decomposition into lemmas can be steered from inside a comfy mathematics markup system. This is some genius UX design, informed by their own research into mathematicians’ workflows (Collins et al. 2026) including the revelation that 2/3 of mathematicians want to keep a steering hand on this tiller.
The harness has the classic minimal-mathematics-machine shape, with a small tool surface: read, write, edit, lean_check, shell, Mathlib search. We can register extra tools via a URL. Runs are persisted as “typed event streams”, supporting replays from dropped sessions, and persistent projects with accumulating scratchpad memory. Model routing is LiteLLM, so we can whack in a self-hosted vLLM endpoint as hosted_vllm/<model>.
Benchmark metrics (e.g. price) are unclear. AxProverBase publishes an ablation table, for example, although it is clearly going to be expensive and difficult to benchmark a Human-in-the-Loop mathematical workflow the same way we can benchmark the automatic ones.
8 Where the compute comes from
What the steps cost to run once built, where fan-out is the only one that costs much.
Build and debug the whole loop on a laptop first — a local IPython kernel for the solver, lean-repl on disk for the prover — and never pay for a GPU until we fan out.1 Modal is a tidy cloud backend to fan out to.
Tokens come from a model behind a scale-to-zero, OpenAI-compatible vllm serve endpoint — a few dozen lines mount a Volume so the weights download once, and the GPU drops to zero between requests, meaning we pay only for the seconds a request actually runs.
Fan-out is then just a .map — one chain per call, each with its own sandbox, all sharing the one endpoint:
The same shape sweeps a whole problem set, or runs a prover’s Pass@k with the Lean verifier as the executor. A heavy tool — a Gröbner basis, a big symbolic integral, a Sage or PARI-GP call — goes on its own cheap CPU modal.Sandbox next to the GPU endpoint, so we aren’t paying for an idle H100 to run SymPy.
8.1 What it costs, rented
The frontier provers aren’t something we rent by the token: the systems at the top of the leaderboard are pilot-only and priced in dollars per problem: on the PutnamBench leaderboard as of 2026-08-31, Aleph’s two entries average $68 and $74 a problem, the second of them solving all 672 at up to $1468 for a single one.2 That is the number a home-built loop has to beat, and one loop has published enough to compare against.
AxProverBase — the minimal agent whose ablations rank the steps above — ships as working code: an off-the-rack Claude Opus 4.5, no fine-tuning, in an iterative compile-and-retry loop with memory and Mathlib search, no recursive decomposition. It reaches 54.7% on PutnamBench, which is 672 problems from the Putnam undergraduate competition formalized in Lean and currently the live proof benchmark.3 That is pass@1 parity with Apple’s decomposition agent Hilbert — 55.9% against AxProverBase’s 54.7% — while spending about 450× fewer tokens per problem, 4.2M against 1880M, at roughly $12.6 a problem against Aleph’s $68–74 average. The parity is at one sample, mind: let Hilbert run out to pass@1840 and it reaches 70%. So the price of the amateur version lands within a factor of a few of the professional one.
What can we rent instead of self-hosting? Fewer options than I’d like. Few specialist maths models are hosted per token, and the one serverless home for the narrow solvers (OpenMath-Nemotron, AceMath4) is Featherless, which caps concurrency per plan — exactly the wrong constraint for fan-out. So sampling these widely usually means self-hosting, on RunPod (from ~$0.27/hr) or Modal (scale-to-zero, which wins on bursty use).5 The exception is at the top of the range: DeepSeek-Prover-V2-671B would need an eight-GPU node to self-host, but Novita meters it cheaply (~$0.70/$2.50 per 1M tokens), so pairing it with a local lean-repl loop is a cheap way into industrial proving.
8.2 On a laptop
When the model runs no code, the whole loop collapses onto a single Mac: maj@k is just \(k\) chat calls and a Counter — no Modal, no .map, no per-chain sandbox. Point it at a local endpoint that can batch the samples and keep the model resident; vllm-mlx is what I actually run, with oMLX as the near-identical fork. A small-but-strong reasoner shines here — many copies for the price of one specialist — and VibeThinker-3B is the obvious candidate, reporting frontier-level verifiable maths at 3B. Treat that as a hypothesis to test on our own problems rather than a fact, for two reasons. The 3B has never been independently reproduced — every secondary source traces back to the same self-report, and MathArena has not run it. Its 1.5B sibling has been, under a standardized harness across ten seeds, where it holds up well but lands below the headline and carries error bars of ±4 to ±8 points. And the one small maths specialist that has been independently benchmarked, QED-Nano, takes 82.5% on AIME 2026 and then 14.06% on ArXivMath, last of twenty-seven: these models generalize to the shape of problem they were tuned on and fall off a cliff just outside it. Fine if our problems are AIME-shaped. A trap otherwise. We wrap it in a plain solve() and call it straight from the loop, not as an MCP tool — which is only needed when the agent is doing the orchestration.
# maj@k against a local oMLX endpoint — the reasoner emits no code, so no sandbox
from openai import AsyncOpenAI
import asyncio, re
from collections import Counter
client = AsyncOpenAI(base_url="http://localhost:8000/v1", api_key="x")
# naïve: [^}]* stops at the first }, so \boxed{\frac{1}{2}} wants a brace-counting scanner
BOXED = re.compile(r"\\boxed\{([^}]*)\}")
async def sample(problem):
r = await client.chat.completions.create(
model="vibethinker-8bit",
temperature=1.0, top_p=0.95, # per request; see below
messages=[{"role": "user", "content": problem}], max_tokens=32000)
hits = BOXED.findall(r.choices[0].message.content or "")
return hits[-1].strip() if hits else None # last boxed = the final answer
async def solve(problem, k=16):
answers = await asyncio.gather(*(sample(problem) for _ in range(k)))
votes = Counter(a for a in answers if a)
return (votes.most_common(1)[0][0] if votes else None), votesTwo details. Set the sampling parameters per request: greedy decoding returns the same answer \(k\) times, so there is nothing to vote over, and the local servers won’t set them for us — vllm-mlx has no per-model sampling keys and falls back to a temperature of 0.7, cooler than VibeThinker wants, and which servers honour which parameters is its own saga. And extraction is easier than it looks — a reasoning model buries its answer under a long <think> trace, but most OpenAI-compatible servers expose the post-think conclusion separately (reasoning_content, given a --reasoning-parser), so the content we regex is the short conclusion and the last \boxed{} is reliably the answer the model is committing to. Return the whole votes tally, not just the winner: the margin is the loop’s confidence signal — 14/16 is a different thing to act on than a 6/5/5 split, where the driver can escalate \(k\) or surface the disagreement instead of committing.
9 What the build taught me
Three things, in decreasing order of how much they surprised me.
The loop is the half I can build, and it is also the half that pays. The ablations put iterative refinement first and memory second, and both of those are a weekend of ordinary Python around models I did not train. Nothing further up the table needed a GPU either — the entire thing debugs on a laptop, and the first dollar goes on fan-out.
But the model I point the loop at cannot be chosen from a benchmark table, because responsiveness to a scaffold appears on no leaderboard. The number that matters is how the model behaves inside my loop — whether it reads a compiler error and does something different next round, whether its scratchpad note is any good. So the shortlist comes from benchmarks and the choice comes from measurement, which means budgeting for the measurement.
There is a second reason, and it is sharper because somebody has measured it. RL post-training buys pass@1 by spending the high-\(k\) tail — an RLVR-tuned model beats its own base at one sample and loses to it at hundreds — and hundreds is the regime fan-out lives in. Measured on a Lean prover, the SFT checkpoint often samples better than the RL checkpoint out of the same lab, purely because it stayed diverse. Standardized re-evaluation of the published gains points the same way, from a completely different direction: RL improvements are mostly modest and overfit small benchmarks, where supervised fine-tuning generalizes more consistently. So where a lab ships both, the SFT one is worth trying as a fan-out backend, and no leaderboard will suggest it: they report the single sample where the RL model wins.
And most of the fiddly work was not mathematics. Deciding whether two answers are the same, pinning Mathlib to the commit the model was trained against, getting a local server to honour a temperature — that is where the days went, and none of it is the part I found interesting. The mathematics was the easy bit, which is either encouraging or ominous depending on the hour.
10 Incoming
- The Journal of Artificial Mathematics
- Bolzano (source) — a colleague’s “automatic researcher” aimed at open problems
- Mathematics and the LLM: 2026 – Coordinate Change
Which chat window we read all this in is a separable and much duller problem, and has its own page.
11 References
Footnotes
Laptop-scale model picks: OpenMath-Nemotron, Qwen2.5-Math, Goedel-Prover, all via MLX.↩︎
Mind the marketing on Aleph. Logical Intelligence frames it as a step beyond LLMs toward a reasoner that “does not think in words”, but by their own telling the benchmarked Aleph is built on an LLM, and the language-free model is unreleased and unbenchmarked.↩︎
That 54.7% is 365/667 against an earlier revision of the benchmark; the current leaderboard scores the same run 365/672, or 54.3%.↩︎
AceMath is CC-BY-NC: non-commercial only, wherever it runs.↩︎
Privacy ranking, briefly: self-hosting on Modal is strongest; the metered shortlist (Novita, Featherless) was pre-filtered to no-train endpoints; keep unpublished proofs off first-party APIs that may train on submitted data, and be thoughtful about OpenRouter, which delegates retention upstream.↩︎
