Skip to main content

Claude vs Cursor

This course builds Northwind's triage service on the Claude API. There is a sibling repository that builds the same four routes, for the same fictional company, against a different API: mrlynn/cursor-triage-api.

Same HTTP spine. Same Zod field names on classification and resolution. Same handbook, same fake order system, same enforceAuthority re-check. The only thing that changes is the primitive underneath.

That is what makes it worth reading. Holding everything else fixed is the only way a comparison says anything: when the service, the taxonomy and the eval set are identical, a difference in the output is a difference between the APIs rather than a difference between two teams' code.

The shape of the difference, in one line. The Messages API is a call. Cursor's SDK is an agent run. Almost everything below follows from that.


Where the Cursor column comes from

Read for the sibling repo, and cited there rather than remembered:

This matters more than it looks. A capability matrix assembled from memory of marketing pages is worth nothing, and it is the default failure mode of every "X vs Y" post you have ever read. Where the Cursor docs publish no number — the Cloud Agents rate limit is the live example — this page says so instead of supplying one.


The capability matrix

CapabilityThis course (Claude)The Cursor twinMaps?
Unit of workPOST /v1/messages, with flags on the requestAgent + RunAgent.create, agent.sendSame product routes, different primitive
Structured outputsoutput_config.format + messages.parse()Prompt for JSON, then Zod in your own processDoes not map. The teaching point, not a hidden bug
Tool useMessages tools / toolRunner, wherever Messages workslocal.customTools, local only. On cloud it is MCP servers, not this callbackPartial
Streamingmessages.stream() token deltasrun.stream() SDKMessage, or onDeltaMaps as "stream the run," not as Messages events
Prompt cacheYou place cache_control breakpoints; ~1024-token minimumcacheReadTokens / cacheWriteTokens are reported; you set nothing, and no TTL knob is documentedObservability maps. Control does not
Token count before a callmessages.countTokens() — free, no inferenceNot documented. Their /v1/estimate refuses to invent oneDoes not map
Usage after a callusage on the Messagerun.usage; dollars via agent.getUsage() (rawCostCents, chargedCents)Maps, different fields and a different billing API
BatchBatches API, half rateNo batch-inference API in the docs read. Cloud agents are a different productDoes not map
MCPLab 10 exposes an MCP server of these toolsAgents consume MCP as a first-class inputMaps, in the other direction
Rate limitsAnthropic rate-limit headers, read off the last callCursor.me() plus published text. Cloud Agents publishes no numberPartial
AuthANTHROPIC_API_KEYCURSOR_API_KEY, user or service-accountMaps. One env var each
Typed errorsAuthenticationError, RateLimitError, …The same, plus AgentBusyError — a state Messages has no equivalent forMaps
ModelsA small catalog and list prices, in src/config.tsCursor.models.list(), liveDo not copy one catalog onto the other

What does not map, in four words each

Constrained decoding. Pre-call token count. A cache breakpoint you place. A half-price batch of classifications.

Each of those is a lab in this course — 2, 5, 7 — and each is a lab that would have to be rewritten from scratch on the other API rather than translated. That is the honest measure of how far apart two APIs are: not the feature checklist, but how much of the curriculum survives the port.


When you would pick each

Pick the Messages API when the job is classify, draft, or run a cheap tool loop against your own backend; when you need a JSON schema the API enforces; when you need count_tokens for admission control; or when a stable handbook wants a cache breakpoint on it. That is nearly all of Northwind triage, which is why this course is built the way it is.

Pick the Cursor Agent SDK when the job is run the agent: a workspace, repo edits, a shell, MCP servers, cloud VMs, opening a PR. Support triage can sit on that primitive — the sibling repo is the proof — but it is a heavier one. You will parse JSON yourself, pay agent-run latency, and go without a free tokenizer.

This half of the page is the part to trust the rest by. A comparison published by one of the two vendors that never concedes anything is marketing with a table in it. A Messages call has no workspace, no shell and no filesystem: the work Cursor's primitive exists for is work this course's runtime cannot do at all. Where that is the job, the choice is not close.


The measured part

Everything above is an argument from documentation. Arguments from documentation are how comparisons go wrong, so both repos ship a command that produces numbers instead.

npm run eval:compare # here
npm run eval:compare # in the Cursor twin
npm run eval:compare:report -- <a> <b> # merge, by case id

Three cases run on both sides — eval-01, eval-04 and eval-05, byte identical in both datasets. Each repo writes a self-describing envelope from its own process, with its own SDK and its own key. The reporter stitches the envelopes afterwards and makes no network calls at all.

That separation is the design. One process holding both SDKs would share an event loop, a DNS cache and a machine, and the latency column — the headline number of the whole comparison — would quietly become a measurement of contention between two clients.

What gets measured

Schema adherence is the sharpest of them. output_config.format makes a malformed response near-impossible here by construction; the Cursor route prompts for the contract and validates afterwards, so a reply that never becomes a TriageResult is a 502 and therefore a rate you can count. The envelope tracks unparseable separately from fail for exactly this reason. A metric that is structurally 1.0 on one side and measured on the other is worth more than any row in the matrix above.

Then: latency p50/p95, accuracy on the shared cases, tokens per decision, cost projected onto Northwind's real load, and the calibration gap from Lab 6.

What the report refuses to do

Three refusals, each enforced in code. Each would make the output shorter and each would make it a lie.

  1. It will not merge envelopes across versions. Two repos drift. A table whose columns mean different things on different rows is worse than no table, because it still looks right.
  2. It will not difference costs across bases. This side estimates from a checked-in price table; the Cursor side reports settled chargedCents. Those are not the same kind of number, and subtracting one from the other produces a slide nobody should trust. They print as separate rows, each under its own basis.
  3. It will not quote accuracy without the sample size. Three cases cannot support a percentage. The disagreement matrix is the output that answers "would I ship this."

Every envelope also carries a not_available map: capability, mapped to why it is missing. Both sides have entries — this one declares that it cannot price a call from an invoice and has no workspace at all. A map that came back empty would mean the comparison had stopped being evidence.

The numbers

Not yet run. A live comparison needs a key on both sides and spends real money, so this section is deliberately empty rather than filled with an estimate.

When it is run, the table lands here, generated by npm run eval:compare:report -- … --out, carrying its own provenance row: the exact command, both SDK versions, the model ids, the repeat count and the sample size. Nothing on this page will be a number that was typed by hand.


What this page will not claim

No cost bake-off has been run. No Claude list price has been multiplied onto a Cursor token. No Cloud Agents requests-per-minute figure has been invented, because none is published.

If you find a number here that you cannot reproduce from a command in one of the two repositories, it is a bug, and it is a more serious one than a wrong answer in a lab.