Adapter SDK guide
Build a typed source adapter in a day. This guide walks through writing a new connector for OrangeRails so end users can connect to your service.
OrangeRails ships with 102 source adapters out of the box: 4 native (Strike, BTCPay, Blink, Bitcoin xpub via Stealth Sync) plus 98 exchanges via the CCXT bridge. If your service isn't in that list, write an adapter and it will be.
What an adapter does
An adapter is one TypeScript module that:
- Declares its slug (the value stored in the
connections.provider_typecolumn). - Declares its credential shape (what fields the OR Connect widget should render: API key, secret, OAuth token, xpub, etc.).
- Implements
discoverWallets— pure pass-through that returns the wallets the credential can see. No DB writes. - Implements
syncByWallets— fetches the latest transactions for the wallets the user selected, returns them in the canonicalNormalizedTransactionshape.
The edge functions handle everything else: auth, persistence, encryption at rest, scheduling, dedup. Your adapter just speaks upstream.
The interface
// supabase/functions/_shared/providers/types.ts
export interface ProviderAdapter {
slug: string;
displayName: string;
description: string;
category: 'lightning_wallet' | 'on_chain_wallet' | 'payment_processor'
| 'exchange' | 'card' | 'mining' | 'bank' | 'lender';
tags?: string[];
popularity?: number;
multiWallet: boolean;
credentialFields: CredentialField[];
discoverWallets(credentials: Record<string, unknown>): Promise<DiscoveredWallet[]>;
syncByWallets(
credentials: Record<string, unknown>,
walletIds: string[],
cursor: string | undefined,
): Promise<SyncResult>;
}
NormalizedTransaction (what your adapter returns):
{
id: string; // provider-side stable id (dedup key)
adapter: string; // matches your slug
direction: 'in' | 'out';
type: 'lightning' | 'onchain' | 'trade' | 'deposit' | 'withdrawal' | 'fee';
amount_sats?: number; // when source is BTC-denominated
amount?: number; // when source is USD/EUR/etc
currency?: string;
description?: string | null;
counterparty?: string | null;
status?: string;
timestamp: string; // ISO 8601
external_payload?: Record<string, unknown>; // raw provider object, opaque
}
Walk through: Strike adapter
The simplest real adapter in the repo. ~150 lines.
supabase/functions/_shared/providers/strike.ts:
- Declares
slug: 'strike',category: 'payment_processor'. - Credential is a single API key with read-only scopes (
partner.account.profile.read+partner.invoice.read) generated at dashboard.strike.me. discoverWalletsreturns one synthetic wallet — Strike accounts are single-account, multi-currency.syncByWalletspages throughGET /v1/invoices?$filter=state in ('PAID','PENDING') and created ge '<cursor>'and emits oneNormalizedTransactionper PAID/PENDING invoice. UNPAID and CANCELLED skipped.
Cursor is the highest created timestamp seen in the batch. Next sync passes it as created ge '<iso>' so Strike does the filtering server-side.
Adapters must be self-contained: no DB calls, no platform-auth concerns. The edge functions handle that layer.
How to add a new adapter
1. Create the file
supabase/functions/_shared/providers/<your-slug>.ts
Implement the ProviderAdapter interface. Use strike.ts or btcpay.ts as templates.
2. Register in the dispatch table
supabase/functions/_shared/providers/dispatch.ts — import your adapter and add it to the PROVIDERS array:
import { yourAdapter } from './<your-slug>.ts';
// ...
const PROVIDERS = [
blinkAdapter,
xpubAdapter,
btcpayAdapter,
strikeAdapter,
yourAdapter, // ← here
...ccxtAdapters,
];
That's it for registration. All three edge functions (or-providers, or-discover-wallets, or-sync) auto-discover it via the registry.
3. Submit a PR
Open against dev in MorningRevolution/orangerails. CI runs bun run build + the providers stress test.
Special-case: CCXT-backed exchanges
If your target is one of the 100+ exchanges CCXT supports, you don't write an adapter at all. Add an entry to _ccxt-manifest.ts (or regenerate via scripts/generate-ccxt-manifest.mjs) and the CCXT base adapter handles it.
Three credential shapes are supported today: apiKey+secret, apiKey+password+secret, apiKey+secret+uid. Exchanges using exotic shapes (DEX wallets, signed messages) need a custom adapter.
What's coming
- Adapter-side webhook ingestion (today everything is poll-based)
- Adapter-side error categorization (so the UI can render "credentials expired" vs "rate limit" differently)
- A standalone
@orangerails/adapter-sdknpm package so adapters can live in their own repos and OR auto-discovers them from a manifest
These are Phase 1 (α) and Phase 2 (β) work. PRs welcome.
See also
- API reference — the edge functions your adapter plugs into
- How authentication works — credential handling end to end
- Self-hosting guide — run with your own adapters