Ingest · data
Data Connectors
Bring your own market-data key and PIT pins the vendor's bars to the same clock the filings use, so an agent reads one timeline instead of two.
- BUILDING
- REST
- Desk plan
What runs today: the license reading for six vendors, the fail-closed cache with the tests that hold it shut, and two adapters — Massive and Tiingo — behind the Desk plan. Sharadar, Databento, Alpaca and EODHD have a reading and no adapter. Options and futures are not implemented, and no design here takes custody of your vendor key.
The artifact
One call, and the block that says how it was handled
Your PIT key authenticates you. Your vendor key rides in a separate header, is read once on the way upstream, and is never written down.
$ curl -s -X POST https://api.pit.aqx.llc/v1/connectors/massive/bars \
-H "Authorization: Bearer $PIT_API_KEY" \
-H "X-Vendor-Key: $YOUR_MASSIVE_KEY" \
-d '{"symbol": "AAPL", "from": "2024-01-02", "to": "2024-01-05"}'
{
"status": "ok",
"count": 4,
"results": [
{"date": "2024-01-02", "o": 187.15, "h": 188.44, "l": 183.885,
"c": 185.64, "vol": 82488700, "vendor_ts": "2024-01-02T05:00:00Z"}
],
"origin": {
"vendor": "massive",
"fetched_at": "2026-08-26T15:04:05Z",
"license_class": "none",
"cached": false
}
}
Shape from docs/BYOK.md; the values are the worked example
in that document. The route is deployed and answers 401 without a key.
license_class reports the class the bytes were actually
handled under, on the same response that says whether anything was
cached, so the two cannot disagree. Every vendor sits at
none today, which means cached is
false on every call the server makes and the payload lives
only in the memory of the request that fetched it.
The failure it prevents
A withdrawn grant changes the cache key, so its bytes stop being served
Most market-data licenses at the individual tier forbid three things at once: sharing the API key, storing the payload, and serving anything derived from it to a third party. A connector that caches vendor bytes for performance and serves them back through our API does all three under the customer's license.
So the cache is keyed on
(customer_id, vendor, license_class, symbol, from, to).
Putting license_class inside the key rather than beside it
means a withdrawn grant changes the key: everything written under the
old class becomes unreachable instead of being served under terms that
have lapsed. customer_id is in there because one
customer's credential fetched those bytes and no second customer may
read them.
The class alone decides whether anything is written, and there is no
configuration flag that turns caching on. At none,
Put refuses with ErrLicenseForbidsCache and
Get always misses. A class rises only through a grant that
names the customer, the vendor, the new class and the document that
permits it — an order-form number, a License Manager row, a
commercial-review thread. A grant with no reference is dropped, and so
is one that would lower a class.
$ go test ./internal/connectors/ -run 'TestEveryVerifiedRowIsNone|TestMatrixMatchesTheDoc' -v
=== RUN TestEveryVerifiedRowIsNone
--- PASS: TestEveryVerifiedRowIsNone (0.00s)
=== RUN TestMatrixMatchesTheDoc
--- PASS: TestMatrixMatchesTheDoc (0.00s)
PASS
ok aqx.llc/pit/internal/connectors 0.176s
Run on 2026-08-27. The first fails the build if any vendor row moves
off none; the second fails it if a vendor appears in the
code and not in the published matrix.
Your key gets the same treatment. It arrives in a header, is held as a
type whose String, GoString,
Format and MarshalJSON all render
[redacted], and is read as raw bytes in exactly one place
per adapter: the Authorization header going upstream. No
vendor response body is folded into our errors, because at least one
vendor echoes a rejected token back in its own error text.
TestVendorKeyIsNeverEchoedOrLogged checks the response
body, every response header and the process log against a canary key,
on success and on each vendor failure.
How it works
Six readings, two adapters
The matrix below is the per-vendor license reading, re-fetched from each vendor's own public pages on 2026-08-26. Where a page did not say, the reading is UNKNOWN and the row stays at the strictest class.
| Vendor | Cache class | Adapter | Why that class |
|---|---|---|---|
| Massive | none |
shipped | Individual terms: personal use, no key sharing, display-only by default, no redistribution of Derived Works. |
| Tiingo | none |
shipped | Starter allows no persistent storage; a backend that sees the token needs commercial review by the vendor's own first-party guidance. |
| Sharadar | none |
none | Personal use only; sharing API keys is not permitted; deletion on termination names caches. |
| Databento | none |
none | Publisher terms pass through and the per-dataset redistribution flags were not readable, so they stay UNKNOWN. |
| Alpaca | none |
none | Personal and non-commercial; no public grant to store on a third-party server or to serve derived quotes. |
| EODHD | none |
none | Non-professional storage is granted on the subscriber's own premises, and display or granting access is barred. |
The matrix is a reading of public pages rather than legal advice, and
counsel reviews each connector before it ships. The full reading, with
the quoted clauses and the retrieval date for each source, is
docs/BYOK.md.
Three clocks, one of them ours
A vendor's bar timestamp — t, date,
ts_event — names the session the bar covers, so at
most it fills event_at. None of the six publishes, on
daily prices, a field saying when that row first became readable.
origin.fetched_at is when this process received the
bytes, and it is the only clock in a connector answer we observed
ourselves.
What a BYOK bar is worth
available_at and committed_at stay empty on
a BYOK bar. Tiingo serves currently-corrected history and Sharadar
stamps a row when it rewrites it, so a bar tells you what a vendor
now says a session looked like. What a reader could have known at
09:31 that morning is a different question, and answering it needs a
source with a capture receipt.
Outside the corpus
A BYOK series is customer-supplied context. The harness can serve it
inside a run and an agent can reason over it, and it never enters
certified_pit, the flat files or
/v1/news. No connector writes into any of them.
Prices as traded
Massive is asked for adjusted=false and Tiingo's raw
open, high, low, close and volume columns are read rather than its
adjusted ones. The two vendors adjust differently, so one normalised
shape carrying two definitions of "close" would disagree with itself,
so you apply the adjustment you want on your own side.
Integration
What the errors mean
invalid_request- Missing
X-Vendor-Key, an unusable symbol, or a malformed window. 400. not_found- A vendor with a license reading and no adapter. 404.
plan_required- A plan below Desk. 403.
unauthorized- The vendor rejected your key.
paramisX-Vendor-Key, so you can tell it apart from your PIT key failing. 401. rate_limited- Your budget for the window is spent. Carries
Retry-After. 429.
These calls reach vendor hosts directly and have no interaction with our SEC crawl budget. Connectors sit on the Desk plan at $499 a month with no meter on top.
Related
Upstream and downstream
- Point-in-Time API
- The certified side of the timeline, where every clock is backed by a capture receipt.
- Evaluation Harness
- Where a BYOK series is useful: you supply the bars, and the receipt records where you said they came from.
- Timestamps reference
- Which clock a query may name, and why a vendor timestamp is never one of them.