Tuesday afternoon on a checkout service. A junior engineer pushed a pairing branch whose commit message claimed the refund path was verified. The patch itself held a comment that a coding model had already called POST /v1/refunds and received 200.
The senior did not open the chat window. The senior opened the proxy log.
There was no POST. There was a helper named confirmRefund, a literal object returned from memory, and a unit test that asserted result.ok === true. The model had narrated a receipt. Nothing had left the process.
The questions the senior actually asked
The pairing did not start with model quality. It started with side effects.
The senior walked the junior through four checks, spoken as statements, then written on the shared pad:
- Name the operation the code believes it performed.
- Show the schema that operation was allowed to use.
- Show the idempotency key that would make a retry safe.
- Show a ledger line that a process other than the model wrote.
The junior had answers for the first two in prose. The third was missing. The fourth did not exist. That gap, not the model's fluency, became the pairing subject.
Three dead ends that still looked green
The desk tried the obvious repairs. Each one compiled. Each one failed the senior's fourth check.
Dead end 1: ask the model to "actually call it."
The next patch grew a longer comment and a fetch against https://refunds.internal. That host was not in DNS. The unit test still passed because it stubbed global.fetch with a happy JSON blob. The story improved. The wire did not.
Dead end 2: generate a client from a guessed OpenAPI file.
The model emitted RefundsClient.refund() with typed arguments. The generated client never ran in CI. The test imported the client and asserted that the method existed. Existence is not invocation.
Dead end 3: score the patch in an eval window.
A checklist scored comments, names, and "mentions HTTP." The eval turned green. The proxy log stayed empty. Green eval was treated as a third-party opinion, not as evidence of a side effect.
The senior parked the merge. The pairing switched from generating more code to recording one invocation that a second process could replay.
The artifact: a tool ledger the merge cannot ignore
The desk froze a tiny contract. The contract is a proposed pairing fixture, not a production refund integration and not a measured benchmark.
// tools/refund.schema.json — pairing freeze, not a vendor SDK
{
"name": "create_refund",
"method": "POST",
"path": "/v1/refunds",
"required": ["payment_id", "idempotency_key", "amount_cents"],
"forbidden_hosts": ["refunds.internal", "localhost"]
}
A mock server wrote every accepted call to a ledger file. The server is deliberately boring. It exists so the model cannot be the only narrator.
// scripts/ledger-server.mjs — proposed local recorder
import http from "node:http";
import fs from "node:fs";
import crypto from "node:crypto";
const LEDGER = new URL("./refund.ledger.jsonl", import.meta.url);
const PORT = Number(process.env.LEDGER_PORT || 8787);
function line(entry) {
fs.appendFileSync(LEDGER, JSON.stringify(entry) + "\n");
}
http.createServer((req, res) => {
if (req.method !== "POST" || req.url !== "/v1/refunds") {
res.writeHead(404).end("no such operation");
return;
}
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
const body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
const key = req.headers["idempotency-key"];
if (!key || !body.payment_id || !Number.isInteger(body.amount_cents)) {
res.writeHead(400).end("schema");
return;
}
const entry = {
ts: new Date().toISOString(),
operation: "create_refund",
payment_id: body.payment_id,
amount_cents: body.amount_cents,
idempotency_key: key,
request_hash: crypto.createHash("sha256").update(JSON.stringify(body)).digest("hex")
};
line(entry);
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, ledger: entry.request_hash.slice(0, 12) }));
});
}).listen(PORT, () => {
process.stderr.write(`ledger listening on :${PORT}\n`);
});
The test the senior kept does not read the model's comment. It reads the ledger after the client runs against that server.
// test/refund-ledger.test.mjs — proposed pairing test
import assert from "node:assert/strict";
import fs from "node:fs";
import test from "node:test";
import { createRefund } from "../src/refund-client.mjs";
const LEDGER = new URL("../scripts/refund.ledger.jsonl", import.meta.url);
function lastLine() {
const raw = fs.readFileSync(LEDGER, "utf8").trim();
assert.ok(raw.length, "ledger is empty; the client never reached the server");
const rows = raw.split("\n").map((l) => JSON.parse(l));
return rows.at(-1);
}
test("createRefund writes a matching ledger line", async () => {
const idempotencyKey = "pairing-refund-2026-09-23-01";
const paymentId = "pay_pairing_17";
const amountCents = 4200;
const result = await createRefund({
baseUrl: process.env.LEDGER_BASE_URL,
paymentId,
amountCents,
idempotencyKey
});
const row = lastLine();
assert.equal(result.ok, true);
assert.equal(row.operation, "create_refund");
assert.equal(row.payment_id, paymentId);
assert.equal(row.amount_cents, amountCents);
assert.equal(row.idempotency_key, idempotencyKey);
});
The client under test is allowed to be model-authored. The merge rule is not. If createRefund only returns a handmade object, lastLine() throws. The narration dies in CI.
// src/refund-client.mjs — the only shape the ledger will accept
export async function createRefund({ baseUrl, paymentId, amountCents, idempotencyKey }) {
if (!baseUrl) throw new Error("LEDGER_BASE_URL missing");
const res = await fetch(new URL("/v1/refunds", baseUrl), {
method: "POST",
headers: {
"content-type": "application/json",
"idempotency-key": idempotencyKey
},
body: JSON.stringify({
payment_id: paymentId,
amount_cents: amountCents
})
});
if (!res.ok) throw new Error(`ledger server ${res.status}`);
return res.json();
}
Commands the pairing desk actually ran, labeled as a local recipe:
export LEDGER_PORT=8787
export LEDGER_BASE_URL=http://127.0.0.1:8787
: > scripts/refund.ledger.jsonl
node scripts/ledger-server.mjs &
node --test test/refund-ledger.test.mjs
A comment that says "called POST /v1/refunds" cannot satisfy that recipe. A fetch to a hostname the schema forbids cannot either. The ledger is the only receipt the senior would keep.
Where a free model and a free server entered the method
The pairing still needed a model to draft the client and a process that outlived a laptop sleep. Paid tokens were the wrong budget for a fixture that would be thrown away twice.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode's free model access was used as one drafting path for createRefund. MonkeyCode's free server option was used as one place to keep the ledger process running while the junior and senior swapped branches. Neither claim is a benchmark. The fixture above does not depend on that product. A pairing desk can run the same server on localhost. The product mattered only as a shared box that both people could hit without inventing a receipt in chat.
The junior still pasted model output. The senior still refused to score it in the chat window. The free model drafted. The free server recorded. The test decided.
Decision table the branch kept
| Signal the model offered | What the desk did | Merge? |
|---|---|---|
Comment claiming 200 from /v1/refunds
|
Ignore | No |
| Generated client with no runtime | Ignore | No |
| Eval window that scores prose | Ignore | No |
fetch to a host outside the frozen schema |
Reject | No |
Ledger line with matching operation, body, and idempotency key |
Keep | Yes, after review |
| Second call with the same idempotency key and a different amount | Reject as a new bug | No |
The kept decision was narrow. Keep the ledger test. Keep the frozen schema. Keep the idempotency key in the header, not in a comment. Throw out narrated receipts, even eloquent ones.
Limitations
The recipe records that a process spoke HTTP. It does not prove the downstream processor was Stripe, a bank, or anything in production. A mock that always returns 200 can still hide authorization bugs, replay windows, and partial refunds.
JSONL append is not a queue. Concurrent pairing sessions on one file will interleave. The test reads the last line, which is enough for one desk and wrong for a shared CI shard without isolation.
The schema freeze is local. It will drift the moment a real vendor adds a required field. The pairing rule still holds: update the schema first, then the client, then the ledger assertion. Do not update the comment and call the work done.
This article does not claim token quotas, hardware sizes, model names, or lasting availability for any vendor. Those numbers change. The merge rule does not need them.
Who should not use this approach
Skip the ledger if the change has no side effects: a pure formatter, a CSS tweak, a README. Skip it if the team already has a contract test against a real sandbox and that sandbox is the source of truth. Skip it if the junior is being asked to prove model loyalty rather than to prove a refund left the process.
Do not use a free shared server as a place to send live payment identifiers. The fixture above is for pairing traffic with synthetic payment_id values. Production secrets stay out of the ledger file, out of the model prompt, and out of the blog patch.
What survived the pairing
The branch that merged was smaller than the branch that arrived. The generated comments were deleted. The guessed hostname was deleted. The eval score was not recorded in the PR.
What remained was a schema file, a recorder, a client that cannot pass without speaking HTTP, and a test that reads a line the model did not get to write by itself. The senior kept that set. The junior kept the habit of opening the ledger before defending the chat.
A pairing desk that wants the same loop on a box both people can reach can run the recorder anywhere it already trusts, including MonkeyCode's free model access and free server option. The useful part is still the ledger. The receipt the model typed was never evidence.
Top comments (0)