TypeScript SDK
@safebank/sdk is the typed client for the SafeBank API. It
handles auth, automatic idempotency, jittered
retries, typed errors,
and local Safe signing.
Install
pnpm add @safebank/sdk
# or: npm i @safebank/sdk / yarn add @safebank/sdk
Requires Node.js 20+ (uses the global fetch). You can inject a custom fetch
for other runtimes.
Create a client
The base URL is inferred from the key prefix (sb_test_ → sandbox), so usually
you pass only the key:
import { SafeBank } from '@safebank/sdk'
const sb = new SafeBank({ apiKey: process.env.SAFEBANK_API_KEY! })
Options (SafeBankOptions):
| Option | Default | Purpose |
|---|---|---|
apiKey | — (required) | sb_test_… / sb_live_…. |
baseUrl | inferred from key | Override the API base URL. |
maxRetries | 3 | Retry attempts on 429/5xx/network errors. |
timeoutMs | 30000 | Per-request timeout. |
fetch | global fetch | Injectable transport (tests / non-Node runtimes). |
Namespaces
The client mirrors the CLI verb tree. Each namespace maps to a REST resource:
| Namespace | Methods | REST |
|---|---|---|
sb.tenants | create, me | /v1/tenants |
sb.keys | create, list, rotate, revoke | /v1/keys |
sb.wallets | create, list, get, fund, transactions, propose, sign, execute, cancel | /v1/wallets |
sb.faucet | listDrips, getDrip | /v1/faucet |
sb.payments | create, list, get, sign, execute, cancel | /v1/payments |
sb.directory | resolve, claimHandle | /v1/directory |
Plus two convenience pollers on the client itself:
await sb.waitForWalletActive(walletId) // resolves when DEPLOYED (or FAILED / timeout)
await sb.waitForPayment(paymentId) // resolves when COMPLETED / FAILED / CANCELLED
End-to-end example
import { SafeBank, signSafeTxHash } from '@safebank/sdk'
const sb = new SafeBank({ apiKey: process.env.SAFEBANK_API_KEY! })
// 1. Create a wallet.
const wallet = await sb.wallets.create({
name: 'Acme Treasury',
owners: [{ address: signerAddress, role: 'ADMIN_OWNER' }],
})
// 2. Fund it (sandbox faucet).
await sb.wallets.fund(wallet.id, { asset: 'usdc', amount: '250' })
// 3. Pay a handle.
const payment = await sb.payments.create({
sourceWalletId: wallet.id,
destination: { type: 'handle', handle: 'volt-components' },
amount: { value: '25.00' },
})
// 4. Sign the safeTxHash and submit (auto-executes at threshold).
const signature = await signSafeTxHash(payment.safeTxHash!, privateKey)
const settled = await sb.payments.sign(payment.id, signature)
Signing
signSafeTxHash(safeTxHash, privateKey) produces a raw secp256k1 signature
over the 32-byte hash — no EIP-191 prefix. This is the single most
load-bearing detail of the payment flow (see the
signing gotcha):
import { signSafeTxHash, generateSigner, addressForPrivateKey } from '@safebank/sdk'
const signer = generateSigner() // { privateKey, address } — sandbox only
const addr = addressForPrivateKey(privateKey) // EIP-55 address for a key
const sig = await signSafeTxHash(safeTxHash, signer.privateKey)
Never sign the safeTxHash with personal_sign / signMessage — the EIP-191
prefix breaks recovery on both the API and Safe. Always use signSafeTxHash.
Error handling
Non-2xx responses throw a typed SafeBankApiError carrying the canonical
envelope fields; transport failures throw
SafeBankNetworkError:
import { SafeBankApiError, SafeBankNetworkError } from '@safebank/sdk'
try {
await sb.payments.create({ /* … */ })
} catch (err) {
if (err instanceof SafeBankApiError) {
console.error(err.code, err.statusCode, err.requestId, err.details)
} else if (err instanceof SafeBankNetworkError) {
console.error('network', err.cause)
} else {
throw err
}
}
Retries and idempotency
- Retries —
429,502,503,504, and network errors are retried up tomaxRetrieswith jittered exponential backoff (250ms → 4s), honoringRetry-After.4xx(other than429) is not retried. - Idempotency — every
POSTgets anIdempotency-Keygenerated once and reused across retries, so nothing double-executes. See Idempotency.
See also
- CLI — the
sbcommand line built on this SDK. - Quickstart — the full loop end to end.
- REST reference — the underlying endpoints.