@aetherwealth/sdk
v0.1.36
Published
Official Aether Wealth SDK: a typed TypeScript client for the Aether Wealth public API — trades, accounts, analytics, alerts, market data, macro calendar, and diary.
Readme
@aetherwealth/sdk
Typed TypeScript client for the Aether Wealth public API. Programmatic
access to your trading journal, accounts, analytics, alerts, market data, and
diary over the public REST surface (/api/public/v1/…).
Keep your API key secret. Authentication uses a public API key (
aw_live_…) sent as a bearer token. Treat it like a password — never commit it or ship it to an untrusted client.
Install
npm install @aetherwealth/sdkQuickstart
import { AetherClient } from '@aetherwealth/sdk'
const client = new AetherClient({
auth: { type: 'apiKey', apiKey: process.env.AETHER_API_KEY! },
// baseUrl defaults to the production API (https://api.aetherwealth.ai).
// Set it only to target staging or a local dev backend.
})
// Every call is authenticated with your API key.
const { data: openTrades } = await client.trades.list({ status: 'OPEN' })
const stats = await client.stats.summary({ pair: 'EURUSD' })Authentication
Each request sends your public API key as Authorization: Bearer <apiKey>. The
key scopes every operation to its owner — you never pass userId in a request
body. auth is a discriminated union (one member today, apiKey) so future
auth modes can be added without breaking existing callers.
new AetherClient({
baseUrl,
auth: { type: 'apiKey', apiKey },
fetchImpl, // optional — inject a custom fetch (tests, proxies)
userAgent, // optional
timeoutMs: 30_000, // optional — per-request timeout (default 30s; 0 disables)
validateResponses: false, // optional — opt-in Zod validation of responses
dangerouslyAllowBrowser: false, // optional — see "Browser use" below
onRequest, onResponse, // optional diagnostics hooks
})The diagnostics hooks receive only { method, url, status, ms } — your API key
travels solely on the Authorization header and never appears in a hook
payload, a thrown error, or the request URL.
Browser use is blocked by default
The API key is a server-side secret. Constructing the client in a
browser-like environment (window.document present) throws — a key shipped to a
browser is visible in the bundle and DevTools and must be treated as public.
Call the API from your server. If you are certain you are in a trusted
non-browser runtime that happens to define window, set
dangerouslyAllowBrowser: true (mirrors the OpenAI SDK).
Resources
| Resource | Methods |
| --- | --- |
| client.trades | list · get · create · update · close · delete · pages · listAll |
| client.accounts | list · create · update · delete · trades · tradesAll |
| client.stats | summary |
| client.alerts | list · createPrice · createTrendline · update · delete · listIndicator · createIndicator · updateIndicator · deleteIndicator |
| client.market | config · calendar · macroSeries · macro |
| client.diary | list · get · upsert · delete · pages · listAll |
List methods return { data, pagination }; single-item methods return the
object directly. All response fields are camelCase.
Pagination
pages() and listAll() async-iterate a paginated resource, auto-advancing
page until the last page (or an empty page), with an infinite-loop backstop:
// One page at a time (keeps pagination metadata):
for await (const page of client.trades.pages({ status: 'CLOSED', limit: 100 })) {
console.log(page.pagination.page, page.data.length)
}
// Every item, flattened across all pages:
for await (const trade of client.trades.listAll({ pair: 'EURUSD' })) {
handle(trade)
}
// Also: client.diary.pages/listAll and client.accounts.tradesAll(accountId)Iteration starts at query.page if given, else page 1.
Timeouts & cancellation
Every request is bounded by timeoutMs (default 30s), overridable per call. A
timeout aborts the request and rejects with AetherTimeoutError (a subclass of
AetherNetworkError, so it's retryable and caught by existing network-error
handlers). You can also pass your own AbortSignal — the request aborts when
either the timeout or your signal fires:
// Client-wide default (30s) or a per-request override via the low-level request():
const slow = new AetherClient({ baseUrl, auth, timeoutMs: 60_000 })
await client.request('/api/public/v1/trades/list', { method: 'POST', body: {}, timeoutMs: 5_000 })
// Caller cancellation (e.g. a UI "cancel" button or request budget):
const controller = new AbortController()
setTimeout(() => controller.abort(new Error('cancelled')), 1_000)
await client.request('/api/public/v1/stats/stats', { method: 'POST', body: {}, signal: controller.signal })A timeout throws AetherTimeoutError; a caller-initiated abort surfaces your
signal's own reason (it is not treated as a transient error to retry).
Idempotency & safe retries
Each create — trades.create, accounts.create, alerts.createPrice,
alerts.createTrendline, alerts.createIndicator — sends an Idempotency-Key
header so a create that the server committed but whose response you never saw
won't duplicate on a re-send. If you don't pass one, the SDK generates a fresh
key per call:
await client.trades.create(input) // auto Idempotency-Key, protects this one callAn auto per-call key protects a single invocation. It does not make a create retry-safe: a new key each attempt means the backend can't dedup. To retry a create safely, pass a stable key so every attempt targets the same record:
import { withRetry } from '@aetherwealth/sdk'
const idempotencyKey = crypto.randomUUID() // generated ONCE, outside the retry
const trade = await withRetry(
() => client.trades.create(input, { idempotencyKey }),
{ maxAttempts: 3 },
)Do not generate the key inside the retried closure — each attempt would get a different key and dedup would be lost.
withRetryonly retries transient failures (AetherRateLimitError,AetherNetworkError,AetherTimeoutError); retrying a mutation is only safe with a stable key.
Errors
Non-2xx responses (and { success: false } envelopes) throw a typed
AetherApiError subclass so you can branch on the failure mode:
import {
AetherRateLimitError,
AetherNotFoundError,
AetherAuthError,
AetherTimeoutError,
} from '@aetherwealth/sdk'
try {
await client.trades.get(id)
} catch (err) {
if (err instanceof AetherNotFoundError) { /* 404 */ }
else if (err instanceof AetherRateLimitError) { /* 429 — err.retryAfterSeconds */ }
else if (err instanceof AetherAuthError) { /* 401 — invalid or missing API key */ }
else if (err instanceof AetherTimeoutError) { /* client timeout — err.timeoutMs / err.elapsedMs */ }
else throw err
}AetherTimeoutError and AetherNetworkError (its parent) never reached the
server; they carry no HTTP status. All HTTP failures are AetherApiError
subclasses.
Optional runtime validation
Compile-time types come from the SDK. Runtime validation is off by default for performance and additive-field tolerance (a backend that adds a field won't break older SDK consumers). Turn it on two ways:
Whole-client — every resource validates its response with a Zod parser and
throws a ZodError on shape drift (unknown fields are still tolerated):
const client = new AetherClient({ baseUrl, auth, validateResponses: true })Per-call — import a parser and validate a single response:
import { parseTrade } from '@aetherwealth/sdk'
const trade = parseTrade(await client.trades.get(id)) // throws on shape driftNot covered
Live candles and AI chat conversations are streamed/tRPC-only on the backend
and are intentionally not part of this REST SDK — reach them through
@aetherwealth/client-core (used by the CLI and MCP server). API-key
management is likewise out of scope: an API key can't provision other keys.
