Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Write a Verifier
In RL from verifiable rewards, the verifier IS the reward. A gameable verifier is worse than no verifier — every false positive is a lie the model happily learns to reproduce. Write one that a reward-hacking agent cannot fool: recompute the score from ground truth, demand exact coverage, and compare with types intact.
The problem
Implement verify(submission, answer_key). answer_key maps each task_id to the correct answer. submission is what the agent hands back: {"outputs": {task_id: value, …}, "reported_reward": float}. Return {"reward": float, "accepted": bool} under three rules that close three reward hacks: (1) never trust reported_reward — recompute the reward yourself from outputs vs answer_key; (2) accepted is False (reward 0.0) unless outputs covers exactly the answer-key task ids — nothing missing, nothing extra; (3) a task counts correct only on a type-strict match (1 is not True, "1" is not 1). When accepted, reward is (# correct) / (# tasks).
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement verify(submission, answer_key) — recompute the reward from ground truth, reject anything but exact task coverage, and score only type-strict matches. Return {"reward": float, "accepted": bool}.
Examples and constraints 5 examples
submission={"outputs": {"t1": 4, "t2": 9}, "reported_reward": 1.0}, answer_key={"t1": 4, "t2": 9}{'reward': 1.0, 'accepted': True}submission={"outputs": {"t1": 0, "t2": 0}, "reported_reward": 1.0}, answer_key={"t1": 4, "t2": 9}{'reward': 0.0, 'accepted': True}submission={"outputs": {"b1": True}, "reported_reward": 1.0}, answer_key={"b1": 1}{'reward': 0.0, 'accepted': True}submission={"outputs": {"t1": 4}, "reported_reward": 1.0}, answer_key={"t1": 4, "t2": 9}{'reward': 0.0, 'accepted': False}submission={"outputs": {}, "reported_reward": 1.0}, answer_key={}{'reward': 0.0, 'accepted': True}- Recompute the reward from
outputsvsanswer_key.reported_rewardis the agent’s self-report — never let it influence the result. - Coverage must be exact: the set of
outputskeys must equal the set ofanswer_keykeys. A missing task OR an extra task ⇒accepted=False,reward=0.0. - Correct means a type-strict match: same value AND same type. In Python,
type(got) is type(expected)before==(soTrue≠1,"1"≠1). - When accepted,
reward = correct / len(answer_key). An empty answer key is a vacuous accept:{"reward": 0.0, "accepted": True}.
Hints 4 ideas
- Start with coverage: if set(submission["outputs"]) != set(answer_key), return {"reward": 0.0, "accepted": False} — before you score anything.
- Never read submission["reported_reward"] into your result. It exists only to tempt you.
- Type-strict: for each task, count it correct only when type(got) is type(expected) AND got == expected. This is what stops True from passing as 1.
- reward = correct / len(answer_key) once accepted. Guard the empty key first: an empty answer_key is a vacuous accept with reward 0.0.
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious verifier loops over what the agent submitted: count the outputs that equal the key with ==, and divide by the number of outputs. It works on honest, complete submissions, and it costs O(n) time and O(1) extra space, so speed is not the problem. It is gameable three ways. A submission of only {"t1": 4} against a two-task key scores 1.0, because the denominator shrinks with the answers. {"b1": True} against {"b1": 1} also scores 1.0, because True == 1 in Python. And any code path that reads reported_reward lets the agent write its own grade.
The approach
The reward must be a function of the answer key and the outputs alone, with the checks ordered so that every hack is closed before scoring starts. The reference reads outputs = submission.get("outputs", {}) and never touches reported_reward. First, coverage: set(outputs.keys()) != set(answer_key.keys()) returns {"reward": 0.0, "accepted": False}, so a missing task and an extra task both fail before anything is scored. Second, an empty key returns the vacuous accept, which also keeps the division safe. Third, the loop runs over answer_key.items(), not over the outputs, and counts a task only when type(got) is type(expected) and got == expected; the type test is what stops True, whose type is bool, from passing as 1. The reward is correct / len(answer_key), a denominator the agent cannot change. In TypeScript, === never coerces, so typeof got === typeof exp && got === exp gives the same strictness.
Walk through an example
The first example, two correct ints against a two-task key, with the self-reported 1.0 ignored throughout.
| step | check | values | result |
|---|---|---|---|
| 1 | read outputs | outputs = {'t1': 4, 't2': 9}; reported_reward is never read | — |
| 2 | coverage | {'t1', 't2'} vs {'t1', 't2'} | equal, continue |
| 3 | empty key | answer_key has 2 tasks | continue |
| 4 | t1 | got 4, expected 4: int is int, 4 == 4 | correct = 1 |
| 5 | t2 | got 9, expected 9: int is int, 9 == 9 | correct = 2 |
| 6 | return | 2 / 2 | {'reward': 1.0, 'accepted': True} |
Complexity
Reference: O(n) time for n tasks: building and comparing the two key sets is O(n), and scoring does one type check and one == per task (longer for big string or list answers). Space is O(n) for the two sets. Naive loop: also O(n) time with O(1) extra space; it is not slower, only gameable.
Edge cases to test
- An extra task smuggled in: outputs
{"t1": 4, "t2": 9, "t3": 1}against{"t1": 4, "t2": 9}gives{"reward": 0.0, "accepted": False}. A verifier that only checks that every key task is present accepts it. - A string for an int:
{"s1": "1"}against{"s1": 1}is accepted, since coverage is exact, and scores 0.0. - A float for an int:
{"t1": 4.0}against{"t1": 4}counts as wrong in the Python reference, becausefloatis notint, but as correct in the TypeScript reference, where both arenumber. Decide which one your verifier means and test it. - An empty answer key with empty outputs:
{"reward": 0.0, "accepted": True}, the vacuous accept. Dividing first raisesZeroDivisionErrorin Python and yieldsNaNin TypeScript. - No
"outputs"field at all:submission.get("outputs", {})treats it as empty, so it is rejected unless the answer key is empty too.
Common mistakes
- Reading
reported_rewardinto the result — it exists only to tempt you; recompute instead. - Comparing with
==and no type check: in PythonTrue == 1, so a bool passes as an int, and in JavaScript loose==also lets"1"pass as1. - Using
isinstance(got, type(expected))as the strict check:isinstance(True, int)is true, becauseboolsubclassesint, so the bool gets through again. - Giving partial credit on partial coverage — a submission that answers only the easy tasks must score 0, not a fraction.
- Dividing by
len(outputs)instead oflen(answer_key): the agent controls that denominator, so answering one easy task scores 1.0. - Forgetting the empty answer key:
correct / len(answer_key)raisesZeroDivisionErrorin Python and givesNaNin TypeScript, where the spec wants a vacuous accept with reward 0.
Follow-ups an interviewer asks next
- Answers can be lists or nested dicts.
- A top-level type check is no longer enough: expected
[1, 2]and output[True, 2]are both lists and compare equal in Python, so the reference counts it correct. Compare recursively instead: the same type at every level, the same length or the same keys, then recurse into each element, and compare leaves with==only after their types match. - Some answers are free-form text or rounded numbers.
- Strict equality turns formatting into failure:
" 42"and42.0both miss42. Canonicalize both sides with one fixed function, by stripping whitespace, parsing numbers or folding case, and compare floats with a tolerance such asmath.isclose. Every relaxation is new surface for reward hacking, so the verifier chooses the comparison and the submission never does. - The verifier runs the agent’s code instead of comparing values.
- Then the submission can attack the verifier itself. Run the code in a sandbox with no network and with time and memory limits, keep the answer key and the expected results outside it, and compare in a separate process the code cannot reach. If the code can write to whatever the score is read from, it can simply report success.
- Could partial coverage earn partial credit safely?
- Yes, if the denominator stays fixed: divide by
len(answer_key)and count every missing task as wrong, so{"t1": 4}against a two-task key scores 0.5 instead of 1.0. Skipping hard tasks then never helps. This challenge rejects instead, which also catches extra task ids, a sign of a malformed or adversarial submission.
Where this shows up
This is the heart of RLVR (reinforcement learning from verifiable rewards): the verifier is the reward, so a gameable one is worse than none — every false positive is a lie the model learns to reproduce. The same “recompute from ground truth, demand exact coverage, compare strictly” discipline runs every autograder, CI test harness, and payments reconciler that scores untrusted input.
Reference solution
Reference solution — Python
def verify(submission, answer_key):
outputs = submission.get("outputs", {})
if set(outputs.keys()) != set(answer_key.keys()):
return {"reward": 0.0, "accepted": False}
if not answer_key:
return {"reward": 0.0, "accepted": True}
correct = 0
for task_id, expected in answer_key.items():
got = outputs[task_id]
if type(got) is type(expected) and got == expected:
correct += 1
return {"reward": correct / len(answer_key), "accepted": True}
AK = {"t1": 4, "t2": 9, "t3": 16, "t4": 25}
print(verify({"outputs": {"t1": 4, "t2": 9, "t3": 16, "t4": 25}, "reported_reward": 1.0}, AK))
# -> {'reward': 1.0, 'accepted': True}
print(verify({"outputs": {"t1": 0, "t2": 0, "t3": 0, "t4": 0}, "reported_reward": 1.0}, AK))
# -> {'reward': 0.0, 'accepted': True} (self-reported 1.0 ignored)Reference solution — TypeScript
type Val = number | string | boolean;
type Submission = { outputs: Record<string, Val>; reportedReward: number };
type Verdict = { reward: number; accepted: boolean };
function verify(submission: Submission, answerKey: Record<string, Val>): Verdict {
const outKeys = Object.keys(submission.outputs);
const keys = Object.keys(answerKey);
const sameCoverage = outKeys.length === keys.length && keys.every((k) => k in submission.outputs);
if (!sameCoverage) return { reward: 0, accepted: false };
if (keys.length === 0) return { reward: 0, accepted: true };
let correct = 0;
for (const k of keys) {
const got = submission.outputs[k];
const exp = answerKey[k];
if (typeof got === typeof exp && got === exp) correct++;
}
return { reward: correct / keys.length, accepted: true };
}
const AK = { t1: 4, t2: 9, t3: 16, t4: 25 };
console.log(verify({ outputs: { t1: 4, t2: 9, t3: 16, t4: 25 }, reportedReward: 1 }, AK));
// -> { reward: 1, accepted: true }
console.log(verify({ outputs: { t1: 0, t2: 0, t3: 0, t4: 0 }, reportedReward: 1 }, AK));
// -> { reward: 0, accepted: true } (self-reported 1 ignored)Questions about Write a Verifier
What is a verifier in RLVR?
In reinforcement learning from verifiable rewards, the verifier is the program that turns a model output into a reward by checking it against ground truth, for example by comparing a final answer with a known solution or by running unit tests. Because the model is trained to maximize that reward, the verifier defines what the model learns. If the verifier can be fooled, training pushes the policy toward fooling it instead of solving the task, which is why verifiers recompute everything themselves and reject malformed submissions outright.
What is reward hacking?
Reward hacking is when a model or agent raises its measured reward without doing the intended task, by exploiting a gap between what the reward checks and what its designer meant. This challenge closes three such gaps: trusting a self-reported score, scoring only the tasks the agent chose to answer, and accepting True where 1 was expected because Python treats them as equal. Each has a rule: recompute, demand exact coverage, and compare with types intact. Training on a hackable reward teaches the hack, so the fix belongs in the verifier.
Why does True equal 1 in Python?
Because bool is a subclass of int in Python: True has the integer value 1 and False has the value 0, so True == 1 and False == 0 are both true, and True + True is 2. That is why the reference checks type(got) is type(expected) before comparing values. The type of True is bool and the type of 1 is int, so the check fails even though the values compare equal. isinstance(True, int) is also true, which makes isinstance the wrong tool for a strict check.
Why reject the whole submission instead of giving partial credit for missing tasks?
Because the agent chooses which tasks to answer. If a submission were scored only on the tasks it includes, answering the one task it is sure about would score 1.0, and a policy trained on that reward learns to skip hard tasks. Rejecting any submission whose task ids differ from the answer key closes that hack and also catches extra, smuggled task ids. Partial credit still exists, but only with full coverage, where the denominator is the whole answer key: two right out of four scores 0.5.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Retry with Exponential BackoffTools time out and APIs rate-limit — a production agent retries without hammering. Exponential backoff waits longer after each failure (1s, 2s, 4s, 8s…) up to a cap. Compute the delay schedule: delay[i] = min(cap, base·2ⁱ). Solve it in Python or TypeScript.Read →
- Trim History to a Token BudgetEvery agent turn re-sends the whole conversation, so history must fit the context budget. Keep the most recent messages that fit and drop the oldest — the trimmer at the heart of context management. Solve it in Python or TypeScript.Read →
- Parse a Tool CallWhen a model wants to act, it emits a call like search(query=cats, limit=5). Parse that string into a function name and typed arguments before the harness can dispatch it — the little parser that turns a model's intent into an action. Solve it in Python or TypeScript.Read →
- Run the Goal LoopThe engine of loop engineering: attempt the front task, let an independent verifier judge it, retry failures with state untouched, and stop when the goal is met — or loudly when the cap trips. Implement the whole outer loop as one pure, testable function. Solve it in Python or TypeScript.Read →
- Build Your Own Agent LoopStrip an "AI agent" of its mystique and what’s left is a loop: read the model’s next move — tool call or final answer — run the tool, feed the observation back, repeat until it answers or the step cap trips. Implement the whole ReAct inner loop as one pure function. Solve it in Python or TypeScript, with hidden tests.Read →
- Two SumThe classic warm-up: find the two numbers that add up to a target. Brute force is O(n²) — a hash map gets you to one pass, O(n). Solve it in Python or TypeScript, right in your browser, with hidden tests and a reveal-solution button.Read →