Get an API key from your profile (Pro+ subscription required), then:
# Node.js β download it, then import the local file curl -o quantlogix-agents.mjs https://quantlogix.ai/sdk/quantlogix-agents.mjs import { QuantLogix } from './quantlogix-agents.mjs'; # Browser ESM β import straight from the URL import { QuantLogix } from 'https://quantlogix.ai/sdk/quantlogix-agents.mjs';
Node can't import over https: β its ESM loader only accepts file: and data: URLs, so download the file first. Browsers import the URL directly.
const ql = new QuantLogix('ql_your_key_here'); // Create a daily pre-market digest agent const { agent } = await ql.agents.create({ name: 'NVDA Pre-Market Digest', mode: 'digest', task_prompt: 'Summarize the overnight setup for NVDA. Include key support/resistance, any catalyst headlines, and the 5-factor signal read.', tickers: ['NVDA'], cadence: 'premarket', }); // Run it now (synchronous β returns the AI result inline) const result = await ql.agents.run(agent.id); console.log(result.result.headline); console.log(result.result.detail); console.log('Confidence:', result.result.confidence); console.log('Tokens used:', result.usage.total_tokens);
const { answer } = await ql.ask({ question: 'Is NVDA oversold right now?', tickers: ['NVDA'], }); console.log(answer.headline); // "NVDA RSI at 28 β technically oversold" console.log(answer.sources); // [{ label, asof? }] β see Sources below console.log(answer.confidence); // 0.82
All requests require a ql_ API key, passed via header (never query params β they leak in logs):
# Preferred: Bearer token curl -H "Authorization: Bearer ql_your_key" \ https://quantlogix.ai/api/v1/agents # Alternative: X-API-Key header curl -H "X-API-Key: ql_your_key" \ https://quantlogix.ai/api/v1/agents
Get your key at /profile. Keys are tier-scoped: Pro unlocks the Agent API and every market-data endpoint, including day/swing signals and microstructure. Institutional covers the same endpoints with higher ceilings β unlimited monthly calls (vs 10,000), 300 req/min (vs 60), a 1M/day AI-token cap (vs 250K), and a fuller /signal payload.
/api/v1/agentsPro+/api/v1/agentsPro+/api/v1/agentsPro+/api/v1/agents?id=<uuid>Pro+/api/v1/agents/runPro+/api/v1/runsPro+/api/v1/runs/<run_id>Pro+/api/v1/agents/connectorsPro+/api/v1/agents/connectorsPro+/api/v1/agents/connectorsPro+/api/v1/agents/connectors?id=<uuid>Pro+/api/v1/agents/connectors?action=testPro+/api/v1/agents/memory?agent_id=<uuid>Pro+/api/v1/agents/memory?agent_id=<uuid>¬e_id=<uuid>Pro+/api/v1/agents/memory?agent_id=<uuid>&all=1Pro+/api/v1/askPro+/api/v1/signal?ticker=NVDAFree/api/v1/news?ticker=NVDA&limit=10Free/api/v1/verified?dataset=convictionAll tiers/signal's payload depends on tier: every key gets the composite score, label, confidence and price, Pro adds the 5-factor scores and indicators breakdown, and Institutional adds microstructure. /news takes limit (default 10, max 50). These are the four market-data endpoints most agents need β the OpenAPI spec lists every gated endpoint, including screener, seasonality, promise-ledger, gap-stats, institutional flow, private companies and CRM; the full REST reference lives at /api-docs.
/api/v1/usageAll tiers/api/v1/openapiPublicPOST /api/v1/agents
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Agent name (max 80 chars) |
task_prompt | string | Yes | What the AI runs each cycle (max 1200 chars) |
mode | string | Default: digest | digest (always send), alert (fire only when condition met), research (deep-dive, Starter+), committee (multi-perspective panel, Starter+) |
tickers | string[] | Optional | Ticker symbols (max 12, uppercase). Extras are dropped and unrecognized symbols silently ignored β not rejected. |
cadence | string | Default: premarket | premarket, close, weekly, intraday (~every 30 min in market hours), once (requires run_at), custom (requires schedule_days + schedule_time) |
run_at | string | Required for once | ISO datetime for the one-shot delivery. Omitting it on create returns 400 run_at_required. The agent auto-disables after it fires. |
schedule_days | string[] | Required for custom | Weekdays the agent runs, e.g. ["mon","wed","fri"] |
schedule_time | string | Required for custom | ET time of day, e.g. "09:45" |
context | string | Optional | Standing background for the AI (max 800 chars) |
channels | object | Default: push+email | { push: true, email: true, telegram: false, discord: false } |
webhook_url | string | Starter+ | HTTPS URL fired on agent results (POST with JSON body) |
connector_ids | string[] | Starter+ | Up to 3 outbound connectors (your REST APIs / MCP servers) the agent may call during its run |
enabled | boolean | Default: true | Enable/disable the agent |
Over-limit values truncate, they don't error. A 200-char name, a 20-symbol tickers list, or a 5th connector is silently trimmed to the documented maximum and the request still returns 201. Read the returned agent back if you need to confirm what was stored.
Tier-gated fields are dropped silently too. Without the agents_advanced entitlement (Starter+), webhook_url and connector_ids are stripped and mode: research/committee falls back to digest β you still get 201 with a working but reduced agent, never a 403.
const { agent } = await ql.agents.create({ name: 'NVDA Oversold Alert', mode: 'alert', // only fires when the condition is met task_prompt: 'Alert me if NVDA RSI drops below 30 AND volume spikes above 1.5x the 20-day average.', tickers: ['NVDA'], cadence: 'intraday', // checks every ~30 min during market hours webhook_url: 'https://your-app.com/webhook/ql-agent', });
POST /api/v1/agents/run β runs synchronously (β€50s) and returns the AI result inline, or in the background with async: true. Never sends push/email. Spends the account's AI token budget.
| Field | Type | Description |
|---|---|---|
id | string | Run a saved agent by UUID |
agent | object | Run an inline draft (same shape as create, no save) |
async | boolean | Return 202 { run_id, poll_url } immediately and execute in the background with a ~5-minute budget β deep research runs that would time out synchronously. Requires a saved agent id. Poll GET /api/v1/runs/<run_id> until status leaves pending. |
One of id or agent is required.
{
"ok": true,
"fired": true,
"reason": null, // why an alert stayed silent, when ok:true & fired:false
"result": {
"headline": "NVDA RSI at 28 β technically oversold with volume 1.8x average",
"detail": "NVDA closed at $425.30, down 3.2%...",
"confidence": 0.82,
"sources": [
{ "label": "NVDA RSI(14) 28.3", "asof": "20:00 ET" }
],
"as_of": "2026-07-11T22:30:00Z"
},
"models": "fast+deep", // reasoning tiers used: "fast", "deep", or "fast+deep"
"tool_calls": 4,
"grounding": null,
"usage": { "total_tokens": 3420, "cost_usd": 0.008912 },
"api_version": "v1"
}
Sources. Each citation is { label, asof? } β label is a human-readable datum (max 120 chars), asof an as-of stamp (max 48 chars) present only when the model supplied one. Up to 8 per result, deduplicated. There are no ticker/metric/value fields.
When the run fails (ok: false) the body carries reason, models, usage and api_version β but no result and no fired. Always branch on ok before reading result.
GET /api/v1/runs β everything your agents produced, newest first: scheduled cron runs (with summaries) and API-triggered runs (with the full structured result). Filter with ?agent_id=<uuid>, page with ?limit= (max 100). A client that misses a webhook can always recover results here.
GET /api/v1/runs/<run_id> β one run; the poll target for async runs. status is pending while an async run executes, then fired / clear / skipped / error.
// Deep research run in the background, then wait for it const { run_id } = await ql.agents.runAsync(agent.id); const run = await ql.runs.wait(run_id); // polls every 5s, up to 5.5 min console.log(run.status, run.result?.headline); // What did my overnight agents find? const { runs } = await ql.runs.list({ limit: 20 }); for (const r of runs) console.log(r.ran_at, r.status, r.summary);
Pro+. Register your own platforms β an HTTPS JSON API or a remote MCP server β and attach them to agents via connector_ids. During a run the agent gets an extra ext_* tool per connector and can pull your data into its reasoning, fully grounded and cited like every other source.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name (also names the agent-side tool, e.g. ext_my_research_api) |
type | string | Yes | rest (HTTPS JSON API) or mcp (JSON-RPC 2.0 over Streamable HTTP). Required on create β the SDK does not validate it client-side, so omitting it fails server-side with 400 type_invalid. |
base_url | string | Yes | HTTPS only, public DNS hosts only β IP literals, ports, credentials, query strings, and private/internal names are rejected |
auth_mode | string | Optional | none, bearer (Authorization: Bearer), or header (custom header via auth_header) |
auth_header | string | With header | Custom header name, [A-Za-z0-9-], max 64 chars |
secret | string | Optional | Write-only. Encrypted at rest (AES-256-GCM), never returned by any read β views carry has_secret only |
allowed_paths | string[] | Optional | REST: path-prefix allowlist, max 20. Empty = nothing callable (fail closed). Each must start with /; malformed entries return 400 allowed_path_invalid. |
allowed_tools | string[] | Optional | MCP: tool-name allowlist, max 20. Empty = every listed tool |
enabled | boolean | Default: true | Enable/disable the connector without deleting it |
Testing a connector. POST /api/v1/agents/connectors?action=test (SDK: connectors.test(id)) probes it live. A failed probe still returns HTTP 200 with { ok: false, error } β check the ok field, not the status. Success returns { ok: true, tools: [...] } for MCP and { ok: true, sample: "..." } for REST.
// Register a connector, verify it answers, attach it to an agent const { connector } = await ql.connectors.create({ name: 'My Research API', type: 'rest', base_url: 'https://api.example.com', auth_mode: 'bearer', secret: process.env.MY_API_TOKEN, allowed_paths: ['/v1/notes', '/v1/positions'], }); await ql.connectors.test(connector.id); // live connectivity probe await ql.agents.update(agent.id, { connector_ids: [connector.id] });
Security posture: per-run call budget (5 external calls), 10s timeout and 256 KB response cap per call, redirects never followed, hostnames re-resolved against public IP space at call time (DNS-rebind guard), and every response is delivered to the model wrapped as untrusted external data β instructions inside connector output are never followed. Up to 10 connectors per account, 3 per agent.
Each agent keeps a private memory: at the end of a run it may save one short note (a level it's watching, a dated expectation) that is injected into its future runs. Notes are isolated per agent β two agents on the same ticker never see each other's memory β and capped at the newest 30.
const { notes } = await ql.agents.memory(agent.id); // [{ id, note, created_at }] await ql.agents.deleteMemoryNote(agent.id, notes[0].id); await ql.agents.clearMemory(agent.id); // wipe it entirely
Mint keys restricted to specific agents β hand a teammate, a bot, or a third-party integration a key that can read and run only the agents you list (max 20), and nothing else: out-of-scope agents 404, creating agents and managing connectors are denied. Pass agent_ids when creating the key on your profile (POST /api/v1/keys), or PATCH an existing key; agent_ids: [] clears the scope.
POST /api/v1/ask β one-shot market question through the same reasoning pipeline as agents. No saved agent needed.
| Field | Type | Required | Description |
|---|---|---|---|
question | string | Yes | Your market question. Minimum 3 chars (shorter returns 400 question_required); anything past 600 chars is silently truncated, so long prompts are answered on their first 600 characters. |
tickers | string[] | Optional | Tickers to scope the analysis (max 8). A comma-separated string is also accepted. Unrecognized symbols are dropped silently, never rejected. |
mode | string | Default: digest | digest (fast) or research (deep-dive). Any other value falls back to digest without an error. |
When an agent has a webhook_url set, each fired scheduled run POSTs this JSON payload to that URL. Note the fields are flat β there is no result wrapper:
{
"event": "agent.run",
"agent": { "id": "uuid", "name": "NVDA Oversold Alert", "mode": "alert" },
"headline": "NVDA RSI at 28 β oversold",
"detail": "...",
"sources": [ { "label": "NVDA RSI(14) 28.3", "asof": "20:00 ET" } ],
"artifact_url": "https://quantlogix.ai/...", // or null
"ran_at": "2026-07-11T14:30:00Z"
}
Two headers accompany every delivery: X-QL-Event: agent.run and X-QL-Agent-Id. There is no confidence field in the webhook body β poll GET /api/v1/runs if you need it.
Only scheduled (cron) runs deliver webhooks. Runs you trigger yourself via POST /api/v1/agents/run never fire one β they return the result inline instead, and deliberately send no push or email either.
Your endpoint should respond with 200 OK within 10 seconds. Failed deliveries are not retried β recover the full structured result from GET /api/v1/runs (GET /api/v1/agents carries only last_status and a truncated last_summary, never the result body).
When you have a webhook signing secret configured on your profile, every delivery is signed: X-QL-Signature: sha256=<hmac> is the HMAC-SHA256 of <X-QL-Timestamp>.<raw body>. The timestamp is part of the signed bytes, so the 5-minute replay window is enforceable β resending a captured delivery with a rewritten X-QL-Timestamp breaks the signature. Verify with the SDK β pass the raw body string, never a re-serialized object:
import { QuantLogix } from './quantlogix-agents.mjs'; // e.g. an Express handler mounted with express.raw({ type: '*/*' }) const valid = await QuantLogix.verifyWebhook({ body: req.body.toString('utf8'), // the RAW bytes, exactly as received signature: req.headers['x-ql-signature'], // HMAC of `<timestamp>.<body>` timestamp: req.headers['x-ql-timestamp'], // required β it is signed; >5 min old is rejected secret: process.env.QL_WEBHOOK_SECRET, // qlw_β¦ from your profile }); if (!valid) return res.status(401).end();
Verification is constant-time and isomorphic (WebCrypto β Node 18+, browsers, edge). Deliveries are unsigned only when no signing secret is configured.
| Tier | Monthly Calls | Per Minute | Agent API | AI Tokens |
|---|---|---|---|---|
| Free | 100 | 5 | β | β |
| Starter | 1,000 | 20 | β | β |
| Pro | 10,000 | 60 | β | 250K/day per key |
| Enterprise | Unlimited | 300 | β | 1M/day per key |
Pro and Enterprise keys carry the same market-data endpoints β the ceilings differ. One exception: POST /v1/simulate (QL Simulator) is exclusive to the enterprise API tier, which Institutional plans mint. See the API reference.
Check your remaining quota at any time β GET /api/v1/usage never consumes quota:
const usage = await ql.usage(); console.log(usage.requests.remaining); // 9421 console.log(usage.ai_tokens.remaining); // 187432 (daily) console.log(usage.tier); // "pro"
The JavaScript SDK (quantlogix-agents.mjs) is a single ESM file with zero dependencies:
| Method | Description |
|---|---|
ql.agents.list() | List all your agents |
ql.agents.create(agent) | Create a new agent |
ql.agents.update(id, patch) | Update an agent (partial) |
ql.agents.delete(id) | Delete an agent |
ql.agents.run(idOrDraft) | Run a saved agent (by id) or inline draft, synchronously |
ql.agents.runAsync(id) | Start a background run (~5 min budget) β returns run_id |
ql.runs.list({ agent_id?, limit? }) | Run history, newest first |
ql.runs.get(id) | One run by id (async-run poll target) |
ql.runs.wait(id, opts?) | Poll a run until it finishes |
ql.agents.memory(id) | The agent's private memory notes |
ql.agents.deleteMemoryNote(id, noteId) / ql.agents.clearMemory(id) | Prune or wipe an agent's memory |
ql.connectors.list() / create(spec) / update(id, patch) / delete(id) / test(id) | Outbound REST/MCP connectors (Pro+) |
ql.ask({ question, tickers, mode }) | One-shot grounded Q&A |
ql.usage() | Quota introspection (free, no quota consumed) |
ql.signal(ticker) | 5-factor composite signal |
ql.news(ticker, limit?) | Curated news with sentiment |
ql.backtest({ ticker, strategy, years? }) | Point-in-time strategy backtest vs buy-and-hold (Starter+) |
ql.backtestLibrary() | Catalog of runnable strategy-backtest IDs (free, unmetered) |
ql.portfolio({ holdings }) | 5-factor roll-up over a book (Pro+) |
ql.portfolioOverlay({ holdings }) | Portfolio Alpha overlay β verdicts + action queue (every tier, holdings-capped) |
ql.verified({ dataset, ticker?, handle?, limit? }) | QL Oracle verified data β conviction / index / creators / record |
ql.signals(tickers) | Batch 5-factor snapshot, up to 25 tickers (Pro+) |
ql.daySignal(ticker) / ql.swingSignal(ticker) | Intraday QDTSS score Β· 2dβ4wk AβD conviction grade (Pro+) |
ql.microstructure(ticker) | OFI / VPIN / smart-money order-flow read (Pro+) |
ql.earningsSetup(ticker) | Pre-earnings setup β implied move + positioning (Pro+) |
ql.screener(filters?) | Programmatic universe screener (Pro+) |
ql.seasonality(ticker) / ql.gapStats(ticker) | Calendar-month seasonality Β· overnight-gap profile (Free) |
ql.promiseLedger(ticker) | Management Promise Ledger β kept / missed / walked-back receipts (Free) |
ql.dca({ ticker, monthly?, years? }) / ql.streetGrades(params?) | DCA analytics Β· analyst-grades-vs-tape leaderboard (Free) |
ql.simulate({ positions }) | QL Simulator β regime-switching Monte Carlo on a what-if book (Institutional) |
ql.institutional(ticker | { cik }) | 13F holders of a ticker, or a fund's book by CIK (Institutional) |
ql.engineModelCard() | The live self-test behind /proof β factor ICs, decay, durability (Pro+) |
ql.researchRequest({ subject }) | Commission a QL Research thesis (Pro+) |
ql.privateCompanies.list / get / news / financials / comparables / ipoReadiness / liquidity / spaces | Private-markets data β the 2,000+-company pre-IPO roster with comps, financials, IPO readiness, secondary liquidity, and the Emerging Spaces board (Pro+) |
ql.privateCompanies.marks(params?) / ql.privateCompanies.markTape(slug | params?) | QL Private Marks β one model-marked valuation per tracked company (last round blended with the daily secondary print and a comps-implied valuation, freshness-weighted, with per-component provenance and a confidence grade) Β· QL Mark Tape β SEC-filed mutual-fund marks: price per share as a fiduciary carries it, the matched-basis quarterly re-mark, and cross-manager dispersion on the same security (Institutional) |
ql.datasets.catalog / manifest / versions / rows | QL Datasets β versioned, hash-verified bulk data in json, jsonl or csv. Every version is an archived QL Rewind snapshot, SHA-256-stamped and publicly chain-verifiable, so rows(slug, { version: '2026-06-01' }) reproduces exactly what that day held. The catalog and manifests are public; full rows are Pro+ (mark-tape is Institutional). Pass ifNoneMatch a previous sha256 and an unchanged version answers { not_modified: true } instead of re-sending the rows β which is what makes polling cheap. |
ql.collab.sheet.read / write Β· ql.collab.note.sections / read / write | QL Collab β documents as APIs: one A1 range of a QL Sheet, one section of a QL Note. Everything outside the addressed range is left untouched, and a write that races another editor returns 409 rather than clobbering it. The key's account must belong to the document's team; anything else is a 404 (Pro+) |
ql.investors.list / get / fit | VC/PE investor directory + startupβinvestor fit scoring (Pro+) |
ql.crm.contacts / deals / tasks / interactions (.list/.create/.update) Β· ql.crm.report() | QL CRM over your key (Pro+) |
ql.webhooks.list / create / test / delete | Outbound webhooks β signed event push to your endpoint. test(id) fires a signed webhook.test delivery so you can prove your verifier works before a real event depends on it (Pro+) |
ql.mesh.publish / subscribe / directory / mine / grant / revoke / call / calls / unpublish | Agent Mesh β publish a native QL agent or connector, subscribe to public listings (QL Deep Dive, VC Deal Scout), grant other accounts scoped access, call via listing_id or qlm_ token. Every call is QL-brokered: endpoints & secrets never cross accounts, payloads are credential-redacted both directions, results arrive untrusted-wrapped, per-grant daily budgets enforced, full audit trail. Grant tokens (qlm_β¦) are returned exactly once (Pro+) |
QuantLogix.verifyWebhook({ body, signature, timestamp, secret, toleranceMs? }) | Verify an inbound webhook signature. timestamp is required β it is part of the signed bytes, and omitting it returns false rather than throwing. toleranceMs defaults to 5 min. Returns a boolean (static; safe to await). |
try { const result = await ql.agents.run(agentId); } catch (err) { if (err.status === 401) console.log('Invalid API key'); if (err.status === 402) console.log('AI budget exhausted, agent cap, or tier upgrade needed'); if (err.status === 403) console.log('Tier too low, key not bound, or out of key scope'); if (err.status === 429) console.log('Rate limit or daily token cap'); if (err.status === 503) console.log('Server timeout or storage unavailable'); if (err.code === 'timeout') console.log('Client timeout β use runAsync() or raise the timeout'); }
Statuses are overloaded β branch on err.code, not status alone. 402 is ai_budget_exhausted (run), cap_reached (agent-count cap on create) or tier_required (connectors). 403 is a tier gate, key_not_bound_to_account, or scoped_key. 503 is timeout or storage_unavailable.
Caveat on err.code: endpoint-level failures return snake_case codes (id_required, cap_reached, not_found, run_failedβ¦), but authentication and quota failures (401 / 403 tier / 429) return a human-readable English sentence in that field instead. For those three, switch on err.status and treat err.code as a display message, not an identifier.
Every failure throws a typed QuantLogixError carrying err.status (HTTP code), err.code (see caveat above), and err.data (the server's JSON body). Transient failures retry automatically with exponential backoff honoring Retry-After β 429/503 for every method, plus 502/504 and network errors for reads; tune with new QuantLogix(key, { maxRetries: 4 }) or disable with { maxRetries: 0 }. A client-side timeout throws err.code === 'timeout' once a request outruns the SDK's timeout (default 55s) and is never retried (the run may still be executing server-side). Deep research runs can exceed that β either raise it (new QuantLogix(key, { timeout: 120000 })) or, better, start the run in the background with agents.runAsync(id) and poll with runs.wait(run_id). Non-ASCII agent names and prompts (em-dashes, accents, emoji) are fully supported.
Get your API key and start creating agents programmatically.
Get API Key β See Plans OpenAPI Spec