Skip to content

Price Pack Format

A pricing pack is the single file costQL produces, and the only thing your app needs to price queries. It is fully self-contained: the schema, the fitted costs, the observed sizes, and any observed outside hosts, with no server, no sidecar, and no network call. This page documents what is inside one, so you can inspect, diff, or debug a pack directly.

You never hand-edit a pack; apps read it through PricingPack.load(). What the pack produces, the quote result, is a separate shape specified in the output contract. This page is the other half: what goes in.

keytypewhat it is
pack_versionnumberpack format version (currently 1).
schema_hashstringa fingerprint of the API’s schema. Every quote echoes it, so a consumer can detect that a pack no longer matches the live schema.
tierstring"T1", "T2", or "T3": the fidelity this pack was built at (see tier fidelity).
currencystringthe cost-unit every price is in: "work_ms" (T2/T3) or "wall_time_ms" (the T1 wall-clock proxy). Never dollars.
introspectionobjectthe API’s GraphQL introspection result, stored so the pack can walk the schema and price any query with no live endpoint.
modelobjectthe cost model: the actual pricing payload (below), including any observed outside hosts (see External calls).
notestringa human-readable reminder of what the pack is (offline, cost-units, never dollars).

This is where a query’s price actually comes from. Every price is computed from these fields alone. The pack stores no per-query prices and does no lookup: price = sum over the query of unit_cost x invocations.

fieldtypewhat it does
unit_cost{resolver_id: number}the cost of one invocation of each resolver, in cost-units. The core of every price.
typical_size{resolver_id: number}the average list size observed for each list-returning resolver during calibration. Price the query with these and you get the typical number.
max_size{resolver_id: number}the worst-case (largest) list size observed. Price with these and you get the safe max. Typical vs safe max is exactly this one swap of sizes (see the FAQ).
default_capnumberthe list size assumed for an edge that declares no size and was never observed. A conservative fallback.
safetynumbera calibration-derived multiplier (>= 1) applied so the safe max never lands below a measured calibration cost.
noise_buffer_msnumbera small additive allowance for measurement noise, in ms. Usually 0.
external_hosts{resolver_id: host}T3 only. For a bounded field that calls an outside service, the host costQL observed at build time (e.g. api.anthropic.com). Surfaced in a quote as external_calls for your app to price (see External calls). Empty when there are none.
batch_groups{resolver_id: loader_key}T3 only. Which resolvers are served by a shared loader, so their repeated work is counted once. Empty at T1/T2.
loader_fns{loader_key: curve}T3 only. The learned batch-size curve for each shared loader (below). Empty at T1/T2.
scan_before_paginate[resolver_id]resolvers whose backend scans a full set before paginating, so a small page can still cost like a large scan. Usually empty.
uncovered_edges[resolver_id]schema edges the calibration panel did not exercise, listed for honesty. Usually empty.
cost_currency, schema_hashmirror the top-level currency and schema_hash; the model is self-describing.

Each shared loader carries a small curve learned from calibration: cost as a function of how many distinct keys a batch pulls.

"<loader key>": { // e.g. a database loader that pulls N rows per batch
"root": "<loader key>",
"kind": "linear", // const or linear: the shape fit from the size sweep
"base": 12.5, // cost at the smallest batch
"slope": 0.4, // added cost per extra distinct key
"cap": 12, // largest batch size ever observed (clamps the input)
"safety": 1.2, // multiplier keeping the curve a safe upper bound
"typical": 9.0, // the typical batch size
"points": [[1, 12.5], [8, 15.7]] // the measured (size, cost) samples it was fit from
}

A network loader tends to fit flat (const): a batched call costs about the same as a single one. A database loader fits a rising curve: a 300-key batch costs more than a 3-key one. Which one it is comes from the data, never assumed.

  • T1 (black box): unit_cost, typical_size, max_size, default_cap, and safety, in wall_time_ms. batch_groups and loader_fns are empty; a black box affords no view of shared work.
  • T2 (per-resolver work): the same shape in work_ms, with per-resolver costs that no longer hide parallel work. Sharing is still inferred, so no observed loader_fns.
  • T3 (work plus sharing trace): adds batch_groups and loader_fns (observed coalescing, priced once on the learned curve), and names any observed outside hosts in external_hosts.

A pack is plain JSON. To see a resolver’s cost and observed sizes:

Terminal window
python -c "import json; m=json.load(open('packs/tmdb_t3.json'))['model']; \
print(m['unit_cost']['Movie.cast'], m['typical_size']['Movie.cast'], m['max_size']['Movie.cast'])"

Or validate the result a pack produces against the frozen shape with costql validate --pack <pack.json> (see the output contract).

costQL gives you a measured estimate, not a guarantee. Whether the prices fit your business is yours to verify. Provided as-is under Apache-2.0, with no warranty.