CI / Test & Type-Check (push) Canceled after 0s
Snapshot of in-progress module work across multiple slices: - Dealer Flow: dealerExposureEngine, dealerMapService, dealerMapExplain, dealerMapIntegrity, dealerMapReplay, dealerStudyEngine, hanStyleLevels - Mirror Portfolio (M21): fundRepository, captureIngest, mirrorAlertProducers, fund holdings strip, live book, position capture ingest - Options: BSM, NormalizedOptionSurface types, OptionsChainRouter, ConvexityGate, option legs panel - Alert producers: vixLevel, rotation, thesis, unlock, portfolioRisk, mirror (fund_capture, fund_13f, mirror_diff) - FINRA short interest adapter + queue integration - SEC company tickers adapter + ingest (symbol search index seed) - Vendor gate (rate-limit-first data plane, ADR-0009) - CUSIP registry, reverse 13F refresh, stock float service - LRU cache, portfolio backtest engine - Frontend: dealer-flow, funds, journal, lab, monitor, plan, portfolio, reports, screener, strategies, theses, guided-start, exits, more pages - Volume profile, workspace profile, visibility-aware poll - ADRs 0010 (mirror math not advice), 0011 (symbol search index) - VENDOR_INTEGRATIONS.md, END_USER_TEST.md - .gitignore: exclude DBs, .DS_Store, local config, agent scratch
3.8 KiB
3.8 KiB
Adding a vendor integration
Rate limits are a first-class product constraint (ADR-0009). Every external vendor - existing and future - must share the same process-wide gate.
If a new integration can call the network without vendorGate, that is a bug.
Required steps
1. Register the family (before any traffic)
import { registerVendorIntegration } from '../services/vendorGate.ts';
registerVendorIntegration({
family: 'polygon', // unique budget name
sourceKinds: ['polygon'], // AdapterQueue source_kind(s)
policy: {
minIntervalMs: 200, // gap between completed calls
maxInflight: 1, // 1 = single-flight
drainJobBudget: 2, // max jobs per drain cycle
hostPattern: 'polygon\\.io', // docs / optional default allowlist
},
});
Call this at process startup (e.g. next to adapter registration in index.ts)
or inside the adapter module top-level so importing the adapter registers it.
2. Extend SourceKind if needed
Add the string to SourceKind in app/server/src/cache/CacheRepository.ts.
3. Implement the adapter (prefer base class)
import { VendorSourceAdapter, type FetchResult } from './SourceAdapter.ts';
import { vendorFetch } from '../services/vendorGate.ts';
export class PolygonAdapter extends VendorSourceAdapter {
readonly sourceKind = 'polygon' as const;
protected async fetchOneUngated(key: CacheKey): Promise<FetchResult> {
// All HTTP:
const resp = await vendorFetch('polygon', url, {
hostAllowlist: /polygon\.io/i,
});
// Library SDKs:
// return withVendorGate('polygon', () => client.get(...));
// but VendorSourceAdapter already gates the whole job — do not double-gate
// unless you make additional calls from a service outside fetchOne.
}
}
Helpers:
| API | Use when |
|---|---|
VendorSourceAdapter |
New queue adapter (auto-gates fetchOne) |
defineVendorAdapter({...}) |
Tiny one-off adapter without a class |
withVendorGate(family, fn) |
SDK / subprocess calls |
vendorFetch(family, url, opts) |
Raw HTTP |
secHttp / secFetch |
SEC only (UA + host rules) |
4. Register with AdapterQueue
adapters.set('polygon', new PolygonAdapter());
// Constructor throws if 'polygon' was not bindSourceKind'd.
5. Schedule (optional)
Add default interval in SCHEDULE_INTERVALS / sourceRatePolicy if the source
should refresh on a timer.
What AdapterQueue enforces
- Construction: every adapter
source_kindmust have a family binding. - Drain: at most
drainJobBudgetjobs per family per cycle. - 429/403: cools all source_kinds in that family + process-wide gate.
- Heal / schedules: skip family while cooling.
What CI enforces
src/services/__tests__/vendorHttpGuard.test.ts:
- Fails if bare
fetch(appears underadapters/,services/,macro/,mirror/outside allowlisted gate modules. - Covers runtime registration of a fictional future vendor.
Checklist for PR review
registerVendorIntegration(or family + bind) presentSourceKindupdated- No bare
fetch/ ungated SDK in the adapter hostAllowlistset for HTTP- Rate-limit errors surface as thrown messages matching
isRateLimitError - Stale/cached path preferred on UI (never stampede from clicks)
Anti-patterns
| Don't | Do |
|---|---|
await fetch(vendorUrl) in an adapter |
vendorFetch(family, url, { hostAllowlist }) |
New TokenBucket per adapter |
Shared vendorGate policy |
Cool only one source_kind on 429 |
Family cool-down (automatic if gated) |
| Call vendor from tRPC handler live | Cache / queue / static fallback |
| Skip registration "just for a prototype" | Register with strict policy even for prototypes |