Guides
TypeScript / Node
The MCP package is convenient, but it isn't required. The REST API is small enough that a hand-rolled client is often the right call — especially for headless agents or worker queues.
Prerequisites
- Node ≥ 20 (uses built-in
fetch). AC_AGENT_KEYin env. Don’t hardcode it — load it fromprocess.envor a secret store.
Drop-in types
Generate from the OpenAPI spec with openapi-typescript, or hand-roll the shapes you need. The minimum:
types.ts
export type ScopeName = 'read' | 'validate' | 'propose';
export interface TradePayload {
action: 'buy' | 'sell';
assetSymbol: string;
assetType: 'crypto' | 'tradfi';
amountUsd: number;
expectedSlippagePct?: number;
}
export interface Violation {
rule: string;
message: string;
limit?: number;
actual?: number;
}
export interface ValidateResponse {
allowed: boolean;
reason: string;
violations: Violation[];
ipsVersion: number | null;
snapshot: Record<string, number | string | null>;
checkedAt: string;
}
export interface ProposeResponse {
proposed: boolean;
idempotent?: boolean;
serviceRequestId?: number;
decision?: ValidateResponse;
message: string;
}Minimal client
ac-client.ts
import { randomUUID } from 'crypto';
import type { ProposeResponse, TradePayload, ValidateResponse } from './types';
const BASE =
process.env.AC_API_BASE ?? 'https://api.advisorscrypto.com';
const KEY = process.env.AC_AGENT_KEY;
if (!KEY) throw new Error('AC_AGENT_KEY is required');
async function call<T>(
method: 'GET' | 'POST',
path: string,
init: { body?: unknown; idempotencyKey?: string } = {},
): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
method,
headers: {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
...(init.idempotencyKey
? { 'Idempotency-Key': init.idempotencyKey }
: {}),
},
body: init.body !== undefined ? JSON.stringify(init.body) : undefined,
});
const text = await res.text();
const parsed = text ? JSON.parse(text) : null;
if (!res.ok) {
const msg =
parsed && typeof parsed === 'object' && 'message' in parsed
? String(parsed.message)
: `HTTP ${res.status}`;
throw new Error(`AC API ${method} ${path} → ${msg}`);
}
return parsed as T;
}
export const ac = {
getMandate: () => call<unknown>('GET', '/api/agent/v1/mandate'),
validate: (payload: TradePayload) =>
call<ValidateResponse>('POST', '/api/agent/v1/policy/validate', {
body: payload,
}),
propose: (payload: TradePayload & { rationale?: string }) =>
call<ProposeResponse>('POST', '/api/agent/v1/trades/propose', {
body: payload,
idempotencyKey: randomUUID(),
}),
};Putting it together
rebalance.ts
import { ac } from './ac-client';
async function rebalanceIfDrift() {
const mandate = (await ac.getMandate()) as {
utilization: { cryptoUsedPct: number };
};
// Sleeve > 70% utilization means we're > 27.5% in a 0.10–0.35 band.
if (mandate.utilization.cryptoUsedPct > 0.7) {
const trim = await ac.propose({
action: 'sell',
assetSymbol: 'BTC',
assetType: 'crypto',
amountUsd: 8500,
rationale: 'Rebalance crypto sleeve toward 20% target.',
});
if (!trim.proposed) {
console.warn('Trim blocked:', trim.decision?.violations);
return;
}
console.log('Pending attestation:', trim.serviceRequestId);
}
}Useful patterns
- Always validate first if you can.
proposealready validates, but a separatevalidatecall is free and lets you log the decision without creating a user-facing attestation when the proposal would be blocked anyway. - Backoff on 429. The global throttle is 100 req/min — interactive agents don’t hit it, but backfill scripts can. Treat 429 as a signal to sleep, not an error.
- Pin
AC_API_BASEfor preview envs. We sometimes run preview backends. SettingAC_API_BASEfrom env lets the same client target prod or preview without code changes.
Last updated 2026-06-15