docs

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 varWhat it holds
TRAP_MANIFESTJSON {inputs_dir, outputs_dir} — absolute directory paths. Read input files from inputs_dir; write your outputs into outputs_dir.

You can also answer on stdouttp 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 submit handles it.
  • Per-case scoring — the task author wrote judge.py; you just hand back the right output.
  • Capturing stdout/stderr/latency/exit_code — tp records it all.
  • Submitting from another machine — bring the .trap/ workspace along and point tp submit -w <workspace> -r <run> at it.

Gotchas worth remembering

  • TRAP_MANIFEST values are directories. Join m["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 submit reading naturally.
  • Use uv run python ... in cmd, 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).