Build a solution
End-to-end: write a solver against an existing task, run it locally, push it to GitHub, submit your score.
We're going to write a solver for the sum-two-numbers task from
build a task. It hands you two ints, expects you
back their sum. About 5 minutes once you have uv + tp.
What you're making
A folder with two files:
solve.py— your program. Reads inputs, writes outputs.trap.yaml— points at the task and declares how your solution runs.
No framework code. tp run orchestrates each case for you.
Step 1 — write solve.py
trap injects TRAP_MANIFEST — a JSON string with two absolute
directory paths. You read this case's inputs from inputs_dir and
write your answer into outputs_dir (you own that directory):
# my-solution/solve.py
import json, os
from pathlib import Path
m = json.loads(os.environ["TRAP_MANIFEST"])
# m = {"inputs_dir": ".../inputs/<case>", "outputs_dir": ".../solution/outputs"}
nums = json.loads((Path(m["inputs_dir"]) / "nums.json").read_text())
(Path(m["outputs_dir"]) / "sum.json").write_text(
json.dumps({"sum": nums["a"] + nums["b"]})
)
One env var is everything:
| Env var | What it holds |
|---|---|
TRAP_MANIFEST | JSON {inputs_dir, outputs_dir} — absolute directory paths. Read input files from inputs_dir; write your outputs into outputs_dir. |
You can also answer on stdout — tp captures stdout/stderr/exit_code
automatically, and the task's judge can read run.stdout. Many LLM
solvers just print the answer and let the judge parse it.
Step 2 — write trap.yaml
A trap.yaml is one solution's config. Invariant settings sit at the top
level; tasks: lists the task bindings you run against, keyed by an
alias that matches the task's id on trapstreet:
# my-solution/trap.yaml
cmd: uv run python solve.py # how your solution runs
profile: # self-reported engine identity → shown on the run page
model: hand-written
framework: stdlib-python
tasks:
sum-two-numbers: # your local name for this binding (convention: the task id)
source: ../sum-task # local path, or git+https://github.com/org/repo@rev
Other top-level fields when you need them: stdin: (pipe one input file
to stdin), setup_cmd: (e.g. uv sync, run once after clone),
name: (leaderboard identity), timeout: (per-case seconds, default
600). Cost tracking is on by default — disable per run with
tp run --no-cost. Model prices come from this site
(GET /api/pricing) and are cached locally, so runs
stay priceable offline; unknown models honestly report cost as unknown
rather than zero.
Step 3 — run it locally
cd my-solution
tp run
uv builds a venv from your pyproject.toml (any will do, even empty),
runs each case, runs the task's judge, prints a summary. All scores
should be 1.0 on basic / negatives / zero.
Step 4 — submit
tp auth login # one-time, see quick start
tp submit
tp submit uploads report.json. It carries your solution's repo +
commit and the task's repo + commit (provenance), so the server locates
the task by content — no task-id argument needed. The CLI prints a
view_url; click it, your row's on the leaderboard.
What you didn't have to think about
- HTTP, auth, retries —
tp submithandles it. - Per-case scoring — the task author wrote
judge.py; you just hand back the right output. - Capturing stdout/stderr/latency/exit_code —
tprecords it all. - Submitting from another machine — bring the
.trap/workspace along and pointtp submit -w <workspace> -r <run>at it.
Gotchas worth remembering
TRAP_MANIFESTvalues are directories. Joinm["inputs_dir"]with your filename; don't expect a pre-built{name → path}map.- You own
outputs_dir. trap never writes there, so the judge sees exactly the files you drop — dynamic output names are fine. - The alias is just your local name for the binding. The server
locates the task by provenance (repo + commit), never by the alias —
using the trapstreet task id is a convention that keeps
tp run/tp submitreading naturally. - Use
uv run python ...incmd, not.venv/bin/python— the first lets uv build the venv. - Public tasks require a public solution repo. Push your solver to
GitHub before
tp submit, or it submits as a local try-out that never ranks (see the local source path).