Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°65

Write a Verifier

Medium AI EngineeringRLVRVerificationAgents

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).

SOLVE IT YOURSELF

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.

YOUR TASK

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}.

CPython · WebAssembly
Examples and constraints 5 examples
EXAMPLE 1
Input submission={"outputs": {"t1": 4, "t2": 9}, "reported_reward": 1.0}, answer_key={"t1": 4, "t2": 9}
Output {'reward': 1.0, 'accepted': True}
every output matches with the right type — full reward
EXAMPLE 2
Input submission={"outputs": {"t1": 0, "t2": 0}, "reported_reward": 1.0}, answer_key={"t1": 4, "t2": 9}
Output {'reward': 0.0, 'accepted': True}
the agent SAYS it scored 1.0 — recompute and ignore it. Outputs are wrong → 0.0
EXAMPLE 3
Input submission={"outputs": {"b1": True}, "reported_reward": 1.0}, answer_key={"b1": 1}
Output {'reward': 0.0, 'accepted': True}
True == 1 in Python — but type-strict, a bool is not an int, so it does NOT count
EXAMPLE 4
Input submission={"outputs": {"t1": 4}, "reported_reward": 1.0}, answer_key={"t1": 4, "t2": 9}
Output {'reward': 0.0, 'accepted': False}
partial coverage — answering only the easy tasks is rejected outright, not given partial credit
EXAMPLE 5
Input submission={"outputs": {}, "reported_reward": 1.0}, answer_key={}
Output {'reward': 0.0, 'accepted': True}
edge case: an empty answer key is a vacuous accept, and the reward is 0.0, not a division by zero
CONSTRAINTS
  • Recompute the reward from outputs vs answer_key. reported_reward is the agent’s self-report — never let it influence the result.
  • Coverage must be exact: the set of outputs keys must equal the set of answer_key keys. 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 == (so True≠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
HINTS — 4 IDEAS
  1. Start with coverage: if set(submission["outputs"]) != set(answer_key), return {"reward": 0.0, "accepted": False} — before you score anything.
  2. Never read submission["reported_reward"] into your result. It exists only to tempt you.
  3. 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.
  4. 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.

stepcheckvaluesresult
1read outputsoutputs = {'t1': 4, 't2': 9}; reported_reward is never read—
2coverage{'t1', 't2'} vs {'t1', 't2'}equal, continue
3empty keyanswer_key has 2 taskscontinue
4t1got 4, expected 4: int is int, 4 == 4correct = 1
5t2got 9, expected 9: int is int, 9 == 9correct = 2
6return2 / 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, because float is not int, but as correct in the TypeScript reference, where both are number. 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 raises ZeroDivisionError in Python and yields NaN in 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_reward into the result — it exists only to tempt you; recompute instead.
  • Comparing with == and no type check: in Python True == 1, so a bool passes as an int, and in JavaScript loose == also lets "1" pass as 1.
  • Using isinstance(got, type(expected)) as the strict check: isinstance(True, int) is true, because bool subclasses int, 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 of len(answer_key): the agent controls that denominator, so answering one easy task scores 1.0.
  • Forgetting the empty answer key: correct / len(answer_key) raises ZeroDivisionError in Python and gives NaN in 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" and 42.0 both miss 42. Canonicalize both sides with one fixed function, by stripping whitespace, parsing numbers or folding case, and compare floats with a tolerance such as math.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.

Finished this one? 0 / 76 Challenges done
NextSpot the Bug in AI Code Next on the “Agents & loop engineering” ladder · rung 7 of 9

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Challenges