Skip to content

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.

POST /v1/connectors/massive/bars 200 · envelope + origin

$ 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/ 2 tests · 0.176s

$ 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.

VendorCache classAdapterWhy 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. param is X-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.