import { randomUUID } from 'crypto';
export class ParaError extends Error {
constructor(
message: string,
public status: number,
public body: unknown,
) {
super(message);
this.name = 'ParaError';
}
/** Return a contextual hint for the AI agent based on the status code. */
get hint(): string {
switch (this.status) {
case 401:
return 'Check that PARA_API_KEY is valid and your server IP is allowlisted in the Para dashboard.';
case 404:
return 'The wallet ID was not found. Verify the ID is correct.';
case 409:
return 'A wallet with this type + scheme + userIdentifier already exists.';
case 429:
return 'Rate limited (10 req/s sustained, 20 burst). Wait and retry.';
default:
return '';
}
}
}
export interface ParaClientOptions {
apiKey: string;
baseUrl?: string;
timeoutMs?: number;
}
export class ParaClient {
private apiKey: string;
private baseUrl: string;
private timeoutMs: number;
constructor(options: ParaClientOptions) {
this.apiKey = options.apiKey;
this.baseUrl = options.baseUrl ?? 'https://api.beta.getpara.com';
this.timeoutMs = options.timeoutMs ?? 30_000;
}
async request<T>(
path: string,
options: { method?: 'GET' | 'POST'; body?: unknown } = {},
): Promise<T> {
const { method = 'GET', body } = options;
const headers: Record<string, string> = {
'X-API-Key': this.apiKey,
'X-Request-Id': randomUUID(),
};
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
try {
const response = await fetch(`${this.baseUrl}${path}`, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
signal: controller.signal,
});
const text = await response.text();
let parsed: unknown = {};
if (text) {
try {
parsed = JSON.parse(text);
} catch {
throw new ParaError('Para returned invalid JSON', response.status, text);
}
}
if (!response.ok) {
throw new ParaError(`Para responded with ${response.status}`, response.status, parsed);
}
return parsed as T;
} finally {
clearTimeout(timer);
}
}
/** Retry with exponential backoff on 429s. */
async requestWithRetry<T>(
path: string,
options: { method?: 'GET' | 'POST'; body?: unknown } = {},
maxRetries = 3,
): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await this.request<T>(path, options);
} catch (err) {
lastError = err;
if (err instanceof ParaError && err.status === 429 && attempt < maxRetries) {
const delayMs = Math.min(1000 * 2 ** attempt, 8000);
await new Promise((r) => setTimeout(r, delayMs));
continue;
}
throw err;
}
}
throw lastError;
}
}