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.