Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/kbot-finance.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Which part of the package?
- [ ] Regulatory verifier (`src/verifier/`)
- [ ] Polymarket adapter
- [ ] SEC EDGAR adapter
- [ ] Alpaca brokerage adapter
- [ ] MCP server (`src/mcp-server.ts`)
- [ ] kbot integration (`src/kbot-tool.ts`)
- [ ] Annex IV exporter (`src/exporters/annex-iv.ts`)
Expand Down
36 changes: 28 additions & 8 deletions packages/kbot-finance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
The open-source substrate for AI agents operating in audited environments —
content-addressed request envelopes, hash-chained append-only audit log,
jurisdiction-aware regulatory verifier (rules-as-code), MCP server, and
engine adapters (Polymarket, SEC EDGAR, more coming). The AI Intelligence
Layer never produces the source-of-truth number — deterministic engines
do, humans approve at material gates, every action is replayable
byte-for-byte under audit.
engine adapters (Polymarket, SEC EDGAR, Alpaca brokerage, more coming). The
AI Intelligence Layer never produces the source-of-truth number —
deterministic engines do, humans approve at material gates, every action
is replayable byte-for-byte under audit.

Apache 2.0. Node 22+. Replit-importable.

Expand All @@ -48,9 +48,10 @@ A reference implementation of three layers that together form an
AI-Native Capital Markets Operating System:

1. **Deterministic engine adapters** — call known-good engines (Polymarket
Gamma in v0.1; QuantLib, NautilusTrader, Aeron, alts-NAV in later versions).
The AI agent cannot compute the number — it can only request one inside a
content-addressed envelope.
Gamma and SEC EDGAR in v0.1; Alpaca brokerage read-only in v0.2; QuantLib,
NautilusTrader, Aeron, alts-NAV in later versions). The AI agent cannot
compute the number — it can only request one inside a content-addressed
envelope.

2. **Regulatory verifier** — Norm-AI-pattern rules-as-code. Every action
passes through before reaching the engine. Failures emit adverse-action
Expand Down Expand Up @@ -82,10 +83,18 @@ cd packages/kbot-finance
npm install
npm run demo # live end-to-end
npm test # unit + integration
npm run test:live # explicit live-smoke against Gamma
npm run test:live # explicit live-smoke against Gamma + Alpaca (skips Alpaca without keys)
KBOT_FINANCE_OFFLINE=1 npm test # CI without network
```

The Alpaca adapter needs a free paper-trading key pair
(`KBOT_FINANCE_ALPACA_KEY_ID` + `KBOT_FINANCE_ALPACA_SECRET_KEY`, or the
`APCA_API_KEY_ID` / `APCA_API_SECRET_KEY` convention Alpaca's own SDKs use)
— sign up at [alpaca.markets](https://alpaca.markets). Defaults to the
paper-trading endpoint; set `KBOT_FINANCE_ALPACA_BASE` to switch to live
only after a compliance sign-off, per the read-only-unless-signed-off
pattern this package uses for every brokerage adapter.

## Architecture (one diagram)

```
Expand Down Expand Up @@ -133,8 +142,12 @@ import {
makeKellyCapRule,
// Engines
polymarket,
edgar,
alpaca,
// Tools
polymarketQuery,
edgarQuery,
alpacaQuery,
} from "@kernel.chat/kbot-finance";
```

Expand Down Expand Up @@ -181,8 +194,14 @@ src/
client.ts # HTTPS client; never throws across boundary
commands.ts # listMarkets / getMarket / listEvents
index.ts
edgar/
types.ts / client.ts / commands.ts / index.ts # SEC filings, read-only
alpaca/
types.ts / client.ts / commands.ts / index.ts # brokerage, read-only
tools/
polymarket-query.ts # The kbot-shaped tool wiring all layers
edgar-query.ts
alpaca-query.ts
demo.ts # End-to-end script (npm run demo)
index.ts # Public surface
test/
Expand All @@ -191,6 +210,7 @@ test/
verifier.test.ts
governance.test.ts
polymarket.live.test.ts # LIVE SMOKE — hits real Gamma
alpaca.live.test.ts # LIVE SMOKE — hits real Alpaca paper API
```

## Strategic positioning
Expand Down
7 changes: 5 additions & 2 deletions packages/kbot-finance/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion packages/kbot-finance/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
"import": "./dist/adapters/edgar/index.js",
"types": "./dist/adapters/edgar/index.d.ts"
},
"./adapters/alpaca": {
"import": "./dist/adapters/alpaca/index.js",
"types": "./dist/adapters/alpaca/index.d.ts"
},
"./exporters/annex-iv": {
"import": "./dist/exporters/annex-iv.js",
"types": "./dist/exporters/annex-iv.d.ts"
Expand All @@ -54,7 +58,7 @@
"mcp": "tsx src/cli.ts mcp",
"test": "vitest run",
"test:watch": "vitest",
"test:live": "vitest run --reporter=verbose test/polymarket.live.test.ts",
"test:live": "vitest run --reporter=verbose test/polymarket.live.test.ts test/alpaca.live.test.ts",
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm run build && npm test"
},
Expand Down Expand Up @@ -93,6 +97,8 @@
"audit",
"mcp",
"polymarket",
"alpaca",
"brokerage",
"compliance-as-code"
],
"dependencies": {
Expand Down
87 changes: 87 additions & 0 deletions packages/kbot-finance/src/adapters/alpaca/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
getAlpacaBase,
getAlpacaCredentials,
type AlpacaOutcome,
type AlpacaError,
} from "./types.js";

/** Low-level Alpaca Trading API HTTP client. Returns discriminated unions; never throws. */
export async function alpacaGet<T>(
path: string,
params: Record<string, string | number | boolean | undefined> = {},
options: { baseUrl?: string; timeoutMs?: number } = {},
): Promise<AlpacaOutcome<T>> {
const credentials = getAlpacaCredentials();
if (!credentials) {
return err({
code: "missing_credentials",
message:
"Set KBOT_FINANCE_ALPACA_KEY_ID + KBOT_FINANCE_ALPACA_SECRET_KEY (or APCA_API_KEY_ID + APCA_API_SECRET_KEY) — a free paper-trading key pair from alpaca.markets works.",
});
}

const base = options.baseUrl ?? getAlpacaBase();
const url = new URL(path.startsWith("/") ? path.slice(1) : path, base + "/");
for (const [k, v] of Object.entries(params)) {
if (v === undefined) continue;
url.searchParams.set(k, String(v));
}

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 10_000);

try {
const res = await fetch(url, {
method: "GET",
headers: {
Accept: "application/json",
"User-Agent": "kbot-finance/0.1",
"APCA-API-KEY-ID": credentials.keyId,
"APCA-API-SECRET-KEY": credentials.secretKey,
},
signal: controller.signal,
});
if (res.status === 401 || res.status === 403) {
return err({
code: "unauthorized",
message: `${res.status} from Alpaca — check the key pair matches the base URL (paper vs live)`,
status: res.status,
});
}
if (res.status === 404) {
return err({ code: "not_found", message: `404 ${url.pathname}`, status: 404 });
}
if (res.status === 429) {
return err({ code: "rate_limited", message: "429 from Alpaca", status: 429 });
}
if (!res.ok) {
const body = await safeText(res);
return err({ code: "http", message: `HTTP ${res.status}`, status: res.status, body });
}
try {
const value = (await res.json()) as T;
return { ok: true, value };
} catch (parseErr) {
return err({
code: "parse",
message: `JSON parse failed: ${(parseErr as Error).message}`,
});
}
} catch (netErr) {
return err({ code: "network", message: (netErr as Error).message });
} finally {
clearTimeout(timeout);
}
}

function err(error: AlpacaError): AlpacaOutcome<never> {
return { ok: false, error };
}

async function safeText(res: Response): Promise<string> {
try {
return (await res.text()).slice(0, 512);
} catch {
return "";
}
}
39 changes: 39 additions & 0 deletions packages/kbot-finance/src/adapters/alpaca/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { alpacaGet } from "./client.js";
import type { AlpacaAccount, AlpacaPosition, AlpacaOrder, AlpacaOutcome } from "./types.js";

/**
* Read-only commands against the Alpaca Trading API.
*
* Order placement intentionally not in v0.1 — read first, governed write
* second, same wedge the Polymarket adapter uses. A brokerage engine is the
* highest-stakes adapter this package ships; it stays read-only until a
* material-gate approval flow for order placement exists.
*/

export async function getAccount(): Promise<AlpacaOutcome<AlpacaAccount>> {
return alpacaGet<AlpacaAccount>("/v2/account");
}

export async function listPositions(): Promise<AlpacaOutcome<ReadonlyArray<AlpacaPosition>>> {
return alpacaGet<ReadonlyArray<AlpacaPosition>>("/v2/positions");
}

export async function getPosition(symbol: string): Promise<AlpacaOutcome<AlpacaPosition>> {
return alpacaGet<AlpacaPosition>(`/v2/positions/${encodeURIComponent(symbol)}`);
}

export async function listOrders(
params: { status?: "open" | "closed" | "all"; limit?: number } = {},
): Promise<AlpacaOutcome<ReadonlyArray<AlpacaOrder>>> {
return alpacaGet<ReadonlyArray<AlpacaOrder>>("/v2/orders", {
status: params.status ?? "open",
limit: params.limit ?? 25,
});
}

/** Alpaca returns numeric fields as strings. Decode once at the normalization boundary. */
export function decodeNumeric(raw: string | undefined): number | null {
if (raw === undefined) return null;
const n = Number(raw);
return Number.isFinite(n) ? n : null;
}
3 changes: 3 additions & 0 deletions packages/kbot-finance/src/adapters/alpaca/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./types.js";
export * from "./commands.js";
export { alpacaGet } from "./client.js";
109 changes: 109 additions & 0 deletions packages/kbot-finance/src/adapters/alpaca/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* Alpaca Trading API types.
*
* Read-only subset calibrated to the account/positions/orders endpoints.
* Alpaca's paper-trading environment is free to sign up for and is the
* default base URL here — a brokerage adapter that defaults to live
* order-eligible credentials would be the wrong failure mode.
*
* Reference: https://docs.alpaca.markets/reference/getaccount
*/

export interface AlpacaAccount {
readonly id?: string;
readonly account_number?: string;
readonly status?: string;
readonly currency?: string;
readonly cash?: string;
readonly portfolio_value?: string;
readonly equity?: string;
readonly last_equity?: string;
readonly buying_power?: string;
readonly regt_buying_power?: string;
readonly daytrading_buying_power?: string;
readonly pattern_day_trader?: boolean;
readonly trading_blocked?: boolean;
readonly account_blocked?: boolean;
readonly created_at?: string;
}

export interface AlpacaPosition {
readonly asset_id?: string;
readonly symbol?: string;
readonly exchange?: string;
readonly asset_class?: string;
readonly side?: string;
readonly qty?: string;
readonly avg_entry_price?: string;
readonly current_price?: string;
readonly market_value?: string;
readonly cost_basis?: string;
readonly unrealized_pl?: string;
readonly unrealized_plpc?: string;
readonly change_today?: string;
}

export interface AlpacaOrder {
readonly id?: string;
readonly client_order_id?: string;
readonly symbol?: string;
readonly asset_class?: string;
readonly side?: string;
readonly type?: string;
readonly qty?: string;
readonly filled_qty?: string;
readonly filled_avg_price?: string;
readonly status?: string;
readonly submitted_at?: string;
readonly filled_at?: string;
readonly canceled_at?: string;
}

/** Adapter outcome — discriminated union. Never throws across the boundary. */
export type AlpacaOutcome<T> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: AlpacaError };

export interface AlpacaError {
readonly code:
| "network"
| "http"
| "parse"
| "not_found"
| "rate_limited"
| "unauthorized"
| "missing_credentials";
readonly message: string;
readonly status?: number;
readonly body?: string;
}

/**
* Paper trading is the default base — a brokerage adapter must not silently
* default to a live-order-eligible endpoint. Set KBOT_FINANCE_ALPACA_BASE
* to switch to https://api.alpaca.markets once a compliance officer has
* signed off on live use, per the read-only-unless-signed-off pattern this
* package follows for every brokerage/pricing engine adapter.
*/
export const ALPACA_PAPER_BASE = "https://paper-api.alpaca.markets";
export const ALPACA_LIVE_BASE = "https://api.alpaca.markets";
export const ALPACA_ADAPTER_VERSION = "alpaca-adapter@0.1.0";

export function getAlpacaBase(): string {
return process.env["KBOT_FINANCE_ALPACA_BASE"] ?? ALPACA_PAPER_BASE;
}

/**
* Alpaca credentials. Namespaced KBOT_FINANCE_ALPACA_* first; falls back to
* the APCA_API_KEY_ID / APCA_API_SECRET_KEY convention Alpaca's own SDKs and
* CLI use, so operators who already have Alpaca configured don't have to
* duplicate keys.
*/
export function getAlpacaCredentials(): { keyId: string; secretKey: string } | null {
const keyId =
process.env["KBOT_FINANCE_ALPACA_KEY_ID"] ?? process.env["APCA_API_KEY_ID"];
const secretKey =
process.env["KBOT_FINANCE_ALPACA_SECRET_KEY"] ?? process.env["APCA_API_SECRET_KEY"];
if (!keyId || !secretKey) return null;
return { keyId, secretKey };
}
2 changes: 2 additions & 0 deletions packages/kbot-finance/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export * from "./governance.js";
export * from "./verifier/index.js";
export * as polymarket from "./adapters/polymarket/index.js";
export * as edgar from "./adapters/edgar/index.js";
export * as alpaca from "./adapters/alpaca/index.js";
export * from "./tools/polymarket-query.js";
export * from "./tools/edgar-query.js";
export * from "./tools/alpaca-query.js";
export * from "./exporters/annex-iv.js";
Loading
Loading