The no-spend guarantee
Requirement (Evan, 2026-09-03): "i don't want to pay any additional pulls.
so most data we can get for free is the target."
Enforced in isFree() / costUsd() in src/databento.ts
Tested by node bin/verify-no-spend.ts
Why code is the only guard
Streaming requests (timeseries.get_range) have no post-hoc usage endpoint.
batch.list_jobs reports cost_usd and bill_id, but only for batch jobs —
the puller streams, so nothing we do appears there. There is no reconciliation
available after the fact.
That makes metadata.get_cost not one guard among several. It is the guard.
The bug that made this necessary
costUsd() originally ended with:
if (!res.ok) return 0;
Zero reads as free. A 500, a rate-limit, an expired key or a dropped
connection would each price a chunk at nothing and let the caller fetch it. The
puller's own catch {} had the same shape — two independent paths from
"something went wrong" to "spend money".
There was a second, subtler version of the same class. costUsd() hardcoded
schema: SCHEMA_TBBO, so when the puller asked "what does ohlcv-1d for
2022-09 cost?" it was answered with the price of tbbo for that month:
$1.26. Real number, real arithmetic, wrong question. The gate refused every
bar chunk older than twelve months while giving a completely plausible reason,
and would have pulled almost nothing. It failed safe — but it failed safe by
accident, and the next such bug might not.
What it does now
costUsd() returns number | null, where null means the price is unknown,
and the only sanctioned question is:
isFree(req) -> { free: cost === 0, cost }
Fetch only when the price is known and exactly zero. Unknown is not free. Small is not free. Schema is threaded through all three call sites and documented at the type so it cannot default silently again.
The tests
bin/verify-no-spend.ts, run against the live API. All four passed
2026-09-03:
| # | check | result |
|---|---|---|
| 1 | billed data prices above zero | AAPL 2022 ticks → $17.31, free=false |
| 2 | a rejected request returns null, not 0 | bad key → cost=null, free=false |
| 2 | a missing key returns null, not 0 | no key → cost=null, free=false |
| 3 | everything in the lake still prices at $0 | 5 venues × 2 bar schemas → all $0.0000 |
| 4 | a fixed reference query still prices at $0 | 968.9M rec / 50.5 GB → $0.0000 |
Test 4 is the important one. It answers "will a 400 GB pull suddenly start billing halfway through". If the free tier were a consumable quota rather than a standing entitlement, the price of a large fixed query would rise as we pull. Re-run it during any long job and compare — a non-zero result means stop.
Test 1 matters more than it looks: a gate that can never see a cost is indistinguishable from a gate that always says free.
What code cannot promise
All of the above is defence against our bugs. It cannot defend against a
change in Databento's pricing, or a path that bypasses isFree() entirely.
Set a spend limit in the Databento portal. It costs nothing and it is the only control that no bug in this repo can defeat.
Current standing
Every batch job on the account: cost_usd = 0, bill_id = null. Total billed
across all of them: $0.00. Everything pulled into the lake so far was
priced at exactly zero before the fetch.