Bridge Core
API Client Setup
typescript
interface BridgeClientConfig {
apiKey: string;
baseUrl?: string;
timeout?: number;
idempotencyKey?: string;
}
class BridgeClient {
private apiKey: string;
private baseUrl: string;
private timeout: number;
constructor(config: BridgeClientConfig) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl || 'https://api.bridge.xyz/v0';
this.timeout = config.timeout || 30000;
}
async request<T>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
endpoint: string,
options: {
body?: Record<string, unknown>;
query?: Record<string, string>;
idempotencyKey?: string;
} = {}
): Promise<T> {
const url = new URL(`${this.baseUrl}${endpoint}`);
if (options.query) {
Object.entries(options.query).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
}
const headers: Record<string, string> = {
'Api-Key': this.apiKey,
'Content-Type': 'application/json',
};
if (options.idempotencyKey) {
headers['Idempotency-Key'] = options.idempotencyKey;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(url.toString(), {
method,
headers,
body: options.body ? JSON.stringify(options.body) : undefined,
signal: controller.signal,
});
clearTimeout(timeout);
if (!response.ok) {
const error = await response.json();
throw new BridgeApiError(error, response.status);
}
return response.json();
} finally {
clearTimeout(timeout);
}
}
// Convenience methods
get<T>(endpoint: string, query?: Record<string, string>): Promise<T> {
return this.request<T>('GET', endpoint, { query });
}
post<T>(endpoint: string, body: Record<string, unknown>, idempotencyKey?: string): Promise<T> {
return this.request<T>('POST', endpoint, { body, idempotencyKey });
}
put<T>(endpoint: string, body: Record<string, unknown>): Promise<T> {
return this.request<T>('PUT', endpoint, { body });
}
delete<T>(endpoint: string): Promise<T> {
return this.request<T>('DELETE', endpoint);
}
}
Error Handling
typescript
interface BridgeApiErrorData {
code: string;
message: string;
location?: string;
name?: string;
errors?: Array<{ code: string; message: string }>;
}
export class BridgeApiError extends Error {
constructor(
public data: BridgeApiErrorData,
public status: number
) {
super(data.message);
this.name = 'BridgeApiError';
}
static fromResponse(data: unknown): BridgeApiError {
const errorData = data as BridgeApiErrorData;
return new BridgeApiError(errorData, 400);
}
get isAuthError(): boolean {
return this.status === 401;
}
get isNotFound(): boolean {
return this.status === 404;
}
get isServerError(): boolean {
return this.status >= 500;
}
get isClientError(): boolean {
return this.status >= 400 && this.status < 500;
}
}
export class BridgeError extends Error {
constructor(
message: string,
public code: string,
public context?: Record<string, unknown>
) {
super(message);
this.name = 'BridgeError';
}
}
Error Codes
typescript
const ERROR_CODES = {
REQUIRED: 'required',
INVALID: 'invalid',
NOT_FOUND: 'not_found',
UNAUTHORIZED: 'unauthorized',
RATE_LIMITED: 'rate_limited',
SERVER_ERROR: 'unexpected',
VALIDATION_ERROR: 'bad_customer_request',
} as const;
function handleBridgeError(error: unknown): never {
if (error instanceof BridgeApiError) {
switch (error.code) {
case ERROR_CODES.REQUIRED:
throw new Error(`Missing required field: ${error.data.name}`);
case ERROR_CODES.INVALID:
throw new Error(`Invalid value: ${error.data.message}`);
case ERROR_CODES.UNAUTHORIZED:
throw new Error('Invalid or missing API key');
case ERROR_CODES.NOT_FOUND:
throw new Error(`Resource not found: ${error.data.message}`);
default:
throw new Error(`API error: ${error.data.message}`);
}
}
if (error instanceof BridgeError) {
throw error;
}
throw new Error(`Unexpected error: ${error}`);
}
Idempotency
typescript
import crypto from 'crypto';
function generateIdempotencyKey(): string {
return crypto.randomUUID();
}
class IdempotencyManager {
private pending = new Map<string, { promise: Promise<unknown>; timeout: NodeJS.Timeout }>();
private cache = new Map<string, { result: unknown; timestamp: number }>();
private readonly TTL = 24 * 60 * 60 * 1000; // 24 hours
async withIdempotency<T>(
key: string,
operation: () => Promise<T>
): Promise<T> {
// Check cache
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.TTL) {
return cached.result as T;
}
// Check pending
const pending = this.pending.get(key);
if (pending) {
return pending.promise as Promise<T>;
}
// Execute operation
const promise = operation()
.then(result => {
this.cache.set(key, { result, timestamp: Date.now() });
this.pending.delete(key);
return result;
})
.catch(error => {
this.pending.delete(key);
throw error;
});
this.pending.set(key, { promise, timeout: setTimeout(() => {
this.pending.delete(key);
}, 60000) });
return promise;
}
clearCache(): void {
this.cache.clear();
}
}
Validation Utilities
typescript
// Address validation by chain
function isValidAddress(address: string, chain: string): boolean {
switch (chain) {
case 'ethereum':
case 'polygon':
case 'base':
case 'arbitrum':
return /^0x[a-fA-F0-9]{40}$/.test(address);
case 'solana':
return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address);
case 'tron':
return /^T[a-zA-Z0-9]{33}$/.test(address);
default:
return false;
}
}
// Currency validation
const VALID_CURRENCIES = ['usd', 'eur', 'mxn', 'usdc', 'usdb', 'usdt'] as const;
type Currency = typeof VALID_CURRENCIES[number];
function isValidCurrency(currency: string): currency is Currency {
return VALID_CURRENCIES.includes(currency as Currency);
}
// Amount validation
function isValidAmount(amount: string): boolean {
const num = parseFloat(amount);
return !isNaN(num) && num > 0 && Number.isFinite(num);
}
// Chain validation
const VALID_CHAINS = [
'ethereum', 'solana', 'polygon', 'base', 'arbitrum', 'tron'
] as const;
function isValidChain(chain: string): chain is typeof VALID_CHAINS[number] {
return VALID_CHAINS.includes(chain as typeof VALID_CHAINS[number]);
}
// Comprehensive request validation
interface ValidatedRequest<T> {
valid: boolean;
errors: string[];
data?: T;
}
function validateTransferRequest(
request: Record<string, unknown>
): ValidatedRequest<CreateTransferRequest> {
const errors: string[] = [];
if (!request.amount || !isValidAmount(request.amount as string)) {
errors.push('Invalid amount');
}
if (!request.source) {
errors.push('Missing source');
} else {
const source = request.source as Record<string, unknown>;
if (!source.payment_rail || !isValidChain(source.payment_rail as string)) {
errors.push('Invalid source chain');
}
if (!source.currency || !isValidCurrency(source.currency as string)) {
errors.push('Invalid source currency');
}
}
if (!request.destination) {
errors.push('Missing destination');
} else {
const dest = request.destination as Record<string, unknown>;
if (!dest.payment_rail || !isValidChain(dest.payment_rail as string)) {
errors.push('Invalid destination chain');
}
if (!dest.to_address && !dest.external_account_id) {
errors.push('Missing destination address or account ID');
}
}
if (errors.length > 0) {
return { valid: false, errors };
}
return { valid: true, errors: [], data: request as CreateTransferRequest };
}
Rate Limiting
typescript
class RateLimiter {
private requests: number[] = [];
private readonly maxRequests: number;
private readonly windowMs: number;
constructor(maxRequests: number = 100, windowMs: number = 60000) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
}
async acquire(): Promise<void> {
const now = Date.now();
// Remove old requests outside window
this.requests = this.requests.filter(t => now - t < this.windowMs);
if (this.requests.length >= this.maxRequests) {
const oldest = this.requests[0];
const waitTime = this.windowMs - (now - oldest);
await new Promise(resolve => setTimeout(resolve, waitTime));
return this.acquire();
}
this.requests.push(now);
}
}
Complete Client Example
typescript
class Bridge extends BridgeClient {
customers = {
createKycLink: (data: CreateKycLinkRequest) =>
this.post<KycLinkResponse>('/kyc_links', data, generateIdempotencyKey()),
list: () => this.get<Customer[]>('/customers'),
get: (id: string) => this.get<Customer>(`/customers/${id}`),
};
wallets = {
create: (customerId: string, chain: string) =>
this.post<Wallet>(`/customers/${customerId}/wallets`, { chain }, generateIdempotencyKey()),
list: (customerId: string) => this.get<Wallet[]>(`/customers/${customerId}/wallets`),
get: (id: string) => this.get<Wallet>(`/wallets/${id}`),
};
transfers = {
create: (data: CreateTransferRequest) =>
this.post<Transfer>('/transfers', data, generateIdempotencyKey()),
list: (params?: ListTransfersParams) =>
this.get<{ count: number; data: Transfer[] }>('/transfers', params),
get: (id: string) => this.get<Transfer>(`/transfers/${id}`),
};
virtualAccounts = {
create: (customerId: string, data: CreateVirtualAccountRequest) =>
this.post<VirtualAccount>(`/customers/${customerId}/virtual_accounts`, data, generateIdempotencyKey()),
list: (customerId: string) =>
this.get<VirtualAccount[]>(`/customers/${customerId}/virtual_accounts`),
};
cards = {
create: (customerId: string, data: CreateCardRequest) =>
this.post<CardAccount>(`/customers/${customerId}/card_accounts`, data, generateIdempotencyKey()),
list: (customerId: string) =>
this.get<CardAccount[]>(`/customers/${customerId}/card_accounts`),
get: (id: string) => this.get<CardAccount>(`/card_accounts/${id}`),
freeze: (id: string, reason: string) =>
this.post<CardAccount>(`/card_accounts/${id}/freezes`, { reason }),
};
externalAccounts = {
create: (data: CreateExternalAccountRequest) =>
this.post<ExternalAccount>('/external_accounts', data, generateIdempotencyKey()),
list: (customerId: string) =>
this.get<ExternalAccount[]>(`/customers/${customerId}/external_accounts`),
delete: (id: string) => this.delete(`/external_accounts/${id}`),
};
webhooks = {
list: () => this.get<{ data: Webhook[] }>('/webhooks'),
create: (url: string) => this.post<Webhook>('/webhooks', { url }, generateIdempotencyKey()),
delete: (id: string) => this.delete(`/webhooks/${id}`),
};
fees = {
updateDefault: (percent: string) =>
this.post<{ summary: string }>('/developer/fees', { default_liquidation_address_fee_percent: percent }),
};
}
// Initialize client
export const bridge = new Bridge({
apiKey: process.env.BRIDGE_API_KEY!,
timeout: 30000,
});
// Usage
async function main() {
try {
// Create customer KYC link
const kycLink = await bridge.customers.createKycLink({
full_name: 'John Doe',
email: 'john@example.com',
type: 'individual',
});
// Create wallet
const wallet = await bridge.wallets.create(kycLink.customer_id, 'solana');
// Create transfer
const transfer = await bridge.transfers.create({
amount: '100',
on_behalf_of: kycLink.customer_id,
source: { payment_rail: 'ach_push', currency: 'usd' },
destination: { payment_rail: 'solana', currency: 'usdc', to_address: wallet.address },
});
console.log('Transfer created:', transfer.id);
} catch (error) {
handleBridgeError(error);
}
}
Constants Reference
typescript
// Payment Rails
const PAYMENT_RAILS = {
fiat: {
us: ['ach_push', 'ach', 'wire'] as const,
eu: ['sepa'] as const,
mx: ['spei'] as const,
br: ['pix'] as const,
},
crypto: ['ethereum', 'solana', 'polygon', 'base', 'arbitrum', 'tron'] as const,
bridge: ['bridge_wallet', 'card'] as const,
} as const;
// Currencies
const CURRENCIES = ['usd', 'eur', 'mxn', 'usdc', 'usdb', 'usdt'] as const;
// Transfer States
const TRANSFER_STATES = ['awaiting_funds', 'completed', 'failed'] as const;
// Customer Statuses
const CUSTOMER_STATUSES = ['active', 'pending', 'suspended'] as const;
// Card Statuses
const CARD_STATUSES = ['active', 'pending', 'inactive', 'frozen'] as const;
Best Practices
- •Use typed client - Leverage TypeScript for type safety
- •Handle errors - Always catch and handle BridgeApiError
- •Use idempotency - Prevent duplicate operations
- •Validate inputs - Use validation utilities before requests
- •Rate limit - Implement rate limiting for production
- •Environment variables - Store API keys securely