Skip to main content

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):

OptionDefaultPurpose
apiKey— (required)sb_test_… / sb_live_….
baseUrlinferred from keyOverride the API base URL.
maxRetries3Retry attempts on 429/5xx/network errors.
timeoutMs30000Per-request timeout.
fetchglobal fetchInjectable transport (tests / non-Node runtimes).

Namespaces

The client mirrors the CLI verb tree. Each namespace maps to a REST resource:

NamespaceMethodsREST
sb.tenantscreate, me/v1/tenants
sb.keyscreate, list, rotate, revoke/v1/keys
sb.walletscreate, list, get, fund, transactions, propose, sign, execute, cancel/v1/wallets
sb.faucetlistDrips, getDrip/v1/faucet
sb.paymentscreate, list, get, sign, execute, cancel/v1/payments
sb.directoryresolve, 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)
danger

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

  • Retries429, 502, 503, 504, and network errors are retried up to maxRetries with jittered exponential backoff (250ms → 4s), honoring Retry-After. 4xx (other than 429) is not retried.
  • Idempotency — every POST gets an Idempotency-Key generated once and reused across retries, so nothing double-executes. See Idempotency.

See also