Skip to content

@openrampkit/server ​

The OpenRampKit server: a web-standard Request -> Response handler plus a small backend API. It runs on Cloudflare Workers, Vercel and Next.js, Node 20+, Bun and Deno.

ts
import { createOpenRamp } from '@openrampkit/server'

const openramp = createOpenRamp({
  secret: process.env.OPENRAMP_SECRET!,
  baseUrl: 'https://app.example.com/api/openramp',
  adapters: [/* ... */],
})

createOpenRamp(config) ​

createOpenRamp throws at startup when secret or admin.token is shorter than 32 characters, when webhooks.secret or tasksToken is shorter than 16 characters, when an adapter targets another API version (the adapter API is version 2), or when two adapters share an id. It logs a warning when treasury has no address: then quotes for app-custody withdrawals use a placeholder sender.

OptionTypeDefaultDescription
secretstringrequiredSigns start URLs. At least 32 characters.
baseUrlstringrequiredPublic URL where the handler is mounted, e.g. https://app.example.com/api/openramp. Its path is stripped from incoming requests. It also builds the return URL, the webhook URLs and the start URLs.
adaptersAdapter[]requiredProvider adapters
storeSessionStorememoryStore()Where sessions live. Use a shared store in production. See Session stores.
livemodebooleanfalseMarks sessions and events as live. Adapters may switch to sandbox when it is false (Coinbase without env). The server checks each adapter.env at start: with true, a sandbox adapter stops the start; with false, a production adapter gets a warning. See Sandbox and production.
policy.maxLegs1 | 22Longest pathway
policy.regionsRegionPolicyallow allApp-wide region policy, applied on top of each leg's policy
policy.methodPriorityRecord<country, string[]>built-inMethod order per country
policy.disabledMethodsstring[]noneMethods never offered
policy.maxAttemptsnumber10The most payment attempts of one session. A failed attempt that leaves no attempt makes the session failed (final).
policy.outputToleranceBpsnumber100 (1%)How much less than the quote a provider may report as a leg's output before the delivery is short (result.delivery). A quote with a minOutput uses that minimum instead. See SessionResult.
policy.hopPreferenceCryptoAsset[]USDC on Base, Arbitrum, Polygon, Optimism, EthereumHop assets for two-leg pathways, most preferred first. See Hops.
webhooks{ url: string; secret: string; retryHours?: number; maxAttempts?: number }noneSigned webhooks to your backend. secret must have at least 16 characters (use 32 random bytes). sweep() retries failed deliveries for retryHours (default 24), or until maxAttempts attempts in all when you set it. Then the event is a dead letter. See Delivery.
tasksTokenstringnoneBearer token for POST /tasks/sweep and GET /health?deep=1. At least 16 characters. Without it, those two are off.
geo(req) => { country?, region? } | undefinedCloudflare / Vercel headersCountry and region for POST /sessions
authorize(req, body) => Promise<CreateSessionInput | null>noneEnables POST /sessions from the browser or another service
cors{ origins: string[] | '*' }same origin onlyAllowed origins for CORS
loggerLoggerconsole (no debug)debug, info, warn, error
returnUrlstring{baseUrl}/returnWhere providers send the user back
fetchtypeof fetchglobal fetchUsed by the server and passed to adapters
timeouts.quotenumber (ms)9000Per quoted pathway
timeouts.webhooknumber (ms)4000Per outgoing webhook
limits.providerCallsPerMinutenumber60Per session: requests to /plan, /target, /quotes, /select, /transitions/*, /deposit-address and /history in one minute. More get 429 RATE_LIMITED.
screenAddress(address, chain) => Promise<boolean>noneWithdraw: check a "To wallet" address. false or an error refuses it (fail closed). See Screen addresses.
screenLightningAddress(address, { cashtag? }) => Promise<boolean>noneWithdraw: check a "To Cash App" target (a $cashtag or a Lightning address). false or an error refuses it (fail closed). With screenAddress set and this hook not set, every Lightning target is refused. See Screen Lightning targets.
screenDepositWalletsbooleanfalseDeposit: also run screenAddress on the connected wallet before a wallet payment starts, on the chain it pays from. A refused wallet gets 403 ADDRESS_REJECTED. A deposit to an address (transfer) has no known sender before the money arrives, so it is not screened.
treasuryTreasuryHooknoneWithdraw with custody: 'app': sends the transactions from your wallet. Throw TreasuryRefusedError (exported) to refuse when nothing was sent; any other error makes the failure final. See Custody.
withdraw.checkBalance{ rpcUrls: Record<string, string>, timeoutMs? }offWithdraw with custody: 'user_wallet' on EVM: read the wallet balance of the source token (ERC-20 balanceOf, or eth_getBalance for native) before quotes and before the first leg starts. An amount above the balance gets 422 AMOUNT_TOO_HIGH. rpcUrls maps a CAIP-2 chain id to a JSON-RPC URL; other chains are not checked. timeoutMs per call, default 3000. RPC errors and timeouts fail open and are logged. See Check the balance on the server.
payPagefalse | { scriptUrl?, title? }on, script from esm.shThe hosted pay page GET /pay/:credential. false turns it off. See The pay link.
admin{ token?, stuckAfterMinutes?, indexDays?, page? }noneAdmin tools. With admin, the server keeps a time index of new sessions. With token (at least 32 characters), the routes /admin/* and the dashboard GET /admin are on. See Admin and observability.
latePayments{ graceHours?, pollMinutes? }{ graceHours: 72, pollMinutes: 10 }After the sweep expires a session whose payment still waits, it keeps polling that payment for graceHours, every pollMinutes. A late payment completes the session and sends session.late_payment. graceHours: 0 turns it off. See Background sweep.
telemetry{ onMetric(name, value, tags) }noneA plain metrics callback: quote latency, start errors, webhook failures, outbox depth, sweep lag. See Metrics.
routing.healthWindowHoursnumber24Hours of provider health counts for the success rate and admin.providers(). See Provider health.
routing.minSamplesnumber20Fewest samples before a rate counts. With fewer, the adapter is neutral in the ranking and the breaker does not open.
routing.breakerFailureRatenumber0.8The circuit breaker skips an adapter when more than this share of its quotes and starts in the last one to two hours failed. 1 turns the breaker off.
routing.breakerCooldownMinutesnumber10How long an open breaker skips the adapter before it lets one probe quote through
routing.breakerbooleantruefalse turns the circuit breaker off. Ranking and failover still work.
historyboolean | { retentionDays?: number }falseThe user's history: GET /sessions/:id/history and the History link in the widget (shown when the user has at least one past session). The server keeps one index of sessions per user from the time you turn it on. Each new session removes up to 20 index entries older than retentionDays (default 90; true keeps 90 days). The route counts against limits.providerCallsPerMinute. Off: the route answers 404 and the widget hides the link. See History.
depositAddresses.widgetbooleanfalseOffer "Your deposit address" in the widget (method deposit_address) and POST /sessions/:id/deposit-address. See Standing deposit addresses.

The default geo lookup reads cf-ipcountry or x-vercel-ip-country (ignoring XX), and x-vercel-ip-country-region for the region (as {country}-{region}).

Return value ​

ts
const openramp = createOpenRamp(config)

openramp.handle(req)          // Promise<Response>. Mount at baseUrl.
openramp.fetch(req)           // Same as handle, for `export default openramp` on Workers, Bun, Deno
openramp.nextHandlers()       // { GET, POST, OPTIONS } for a Next.js App Router catch-all route

await openramp.sessions.create(input)  // Promise<CreatedSession>; with externalId: Promise<CreatedSession | ExistingSession>
await openramp.sessions.retrieve(id)   // Promise<Session | null>: the backend view (PublicSession plus userId and metadata)
await openramp.sessions.refresh(id)    // Promise<Session | null>: ask the active leg's adapter for status now
await openramp.sessions.cancel(id, { reason? })        // Promise<Session | null>: cancel before money moved (reason default 'requested_by_app'); 409 otherwise (see POST /sessions/:id/cancel)
await openramp.sessions.payLink(id, { ttlMinutes? }) // Promise<PayLink | null>: { id, url, expiresAt }, a signed link to the pay page
await openramp.sessions.revokePayLink(id, linkId)    // Promise<boolean>: make one pay link stop working; false when the session does not exist

await openramp.sweep({ limit? })       // Promise<SweepResult>: retry webhooks, poll standing addresses, refresh open payments, expire sessions

await openramp.depositAddresses.get({ userId, origin, destination, adapterId?, dedicatedDestination? }) // Promise<StandingAddress>: the permanent address of a user and route
await openramp.depositAddresses.list(userId)                                      // Promise<StandingAddress[]>: newest first

await openramp.webhooks.verify(req, rawBody) // Promise<boolean>: verify a webhook this server sent
await openramp.webhooks.replay(sessionId)    // Promise<number>: send the dead letters of a session again

await openramp.admin.list({ direction?, state?, olderThan?, stuck?, limit?, cursor? }) // Promise<AdminListResult>
await openramp.admin.get(id)                  // Promise<AdminSession | null>: legs, attempts, outbox, refs, timeline
await openramp.admin.findByRef(provider, ref) // Promise<AdminSession | null>
await openramp.admin.findByTx(chain, txHash)  // Promise<AdminSessionSummary[]>: a transaction of any role, in any attempt
await openramp.admin.stats({ since? })        // Promise<AdminStats>
await openramp.admin.resolve(id, state, note) // Promise<AdminSession>: force a final state, with an audit note and a webhook
await openramp.admin.replayWebhooks(id)       // Promise<{ queued: number }>
await openramp.admin.providers()              // Promise<ProviderHealth[]>: counts, success rate, recent failure rate and breaker state per adapter

handle never throws. Errors become JSON responses (see HTTP routes). It answers OPTIONS with 204 and the CORS headers.

sessions.revokePayLink(id, linkId) takes the id that payLink returned. The pay page and the session routes then refuse that link. Other links of the session and the client secret keep working. A session keeps at most 100 revoked link ids. See POST /sessions/:id/pay-link/revoke.

sessions.refresh(id) skips the 2-second rate limit that browser polls have. It sends any webhooks that become due. You rarely need it: sweep() refreshes every open session.

webhooks.verify returns false when config.webhooks is not set.

webhooks.replay(sessionId) sends again the events of one session whose retries stopped (dead letters). They keep their event ids and get a new retry window. It returns the number of events, or 0 when the session has none.

depositAddresses.get returns a permanent deposit address for one user and route, and makes it on the first call. Each deposit to it becomes a deposit session with the usual webhooks. See Standing deposit addresses.

admin.* is for your backend and for operators. list, stats and findByTx read the time index, which the server keeps only when config.admin is set. See Admin and observability.

The type of the returned object is exported as OpenRamp.

Background sweep ​

Users close tabs, and webhook deliveries fail. openramp.sweep() does the background work. Run it every minute or so:

Each run does these things:

  1. Retries failed webhooks. Each event is saved in its session record (the outbox), and the session id goes on the outbox queue. A delivery that fails (no 2xx answer, or a timeout) stays there. The sweep sends it again when it is due. The wait starts at 30 seconds and doubles after each failed attempt, up to 2 hours. After webhooks.retryHours (default 24), or webhooks.maxAttempts attempts when you set it, the event becomes a dead letter in the session record. The server logs webhook moved to dead letter after retries as an error. openramp.webhooks.replay(sessionId) sends the dead letters again.
  2. Polls standing deposit addresses. For each standing address that is due, it asks the adapter for new deposits, and makes one session per new deposit. An address is polled every 30 seconds after a deposit in the last hour, then less often, up to every 10 minutes. After 90 days with no deposit and no depositAddresses.get, the sweep stops polling it until the next get.
  3. Refreshes open payments. For each open session with an active payment, it asks the active leg's adapter for status, like sessions.refresh(id). This settles legs without provider webhooks (Relay) after the user leaves. It also asks for the status of earlier attempts that the user left with restart and that still wait. When one of them was paid, the session completes with it.
  4. Expires idle sessions. A session past its expiry moves to EXPIRED when no payment started, or when the active leg still waits for the user (requires_action). The server sends session.expired. A leg that the provider is processing is not expired: the sweep refreshes it instead.
  5. Polls late payments. A bank transfer or a deposit to an address can arrive after the session expired. When the expired session's active leg has a provider ref and the adapter has status(), the session goes on the grace list. The sweep polls it every latePayments.pollMinutes (default 10) for latePayments.graceHours (default 72). When the payment arrives, the session moves on from EXPIRED: it completes (or the next leg starts), and the server sends session.late_payment (with reason: 'after_expiry') and then session.succeeded. Set graceHours: 0 to turn it off. A provider webhook for the waiting leg has the same effect inside the grace window. Only money that arrives on the leg that waited at expiry moves the session on. After the window (or with graceHours: 0), a late payment changes the leg data only: the session stays EXPIRED, and the server sends session.late_payment with reason: 'after_grace'. A failure event, an event for an earlier attempt, or a browser request does not change an expired session.
ts
type SweepResult = {
  webhooks: { retried: number; delivered: number; dropped: number; pending: number }
  sessions: { checked: number; changed: number; expired: number; open: number; grace: number } // grace: expired sessions polled for a late payment
  standing: { checked: number; deposits: number } // standing addresses polled, and new deposits (one session each)
}

limit (default 50) caps the sessions with webhooks to retry and the open sessions to check in one run. The rest wait for the next run. webhooks.pending is the number of sessions on the outbox queue. A session can stay there for up to 30 seconds after its last event was sent; the next due run removes it.

The outbox queue and the open-session list are store queues: one entry per session id, with an atomic add. A session that is added while a sweep runs is never lost. Each run takes the entries that waited longest, so every open session gets its turn (round robin), also with more than limit open sessions. The run holds a 10-minute lease on what it took, so a second sweep at the same time skips those entries.

At-least-once delivery

A delivery can still arrive more than once, for example after a timeout. A repeat has the same event id. Deduplicate by event id (openramp-id) in your backend. See Credit exactly once.

With admin set, each run also removes days of the admin time index that are older than admin.indexDays.

The server tracks only the sessions it creates. A session goes back on the open-session list after a restart, and when a provider event arrives for it.

Provider health ​

The server counts, per adapter and hour, quotes that worked or failed, starts that worked or failed, and legs that succeeded or failed. A leg counts once per session, attempt and leg, also when a save is retried. A leg that failed because of the user (for example PAYMENT_FAILED, KYC_REJECTED, PROVIDER_DECLINED or QUOTE_EXPIRED) is not a provider failure. The counts live in the store's KV space (one key per adapter, health:{adapterId}), for routing.healthWindowHours (default 24). A write is one read and one write with no lock: two writes at the same time can lose one update, and a store error only skips the update. A request never fails because of these counts.

An error that is an answer from a working provider is not a failure: REGION_UNSUPPORTED, AMOUNT_TOO_LOW, AMOUNT_TOO_HIGH, NO_QUOTES, QUOTE_EXPIRED, PROVIDER_DECLINED, KYC_REJECTED, BAD_REQUEST, ADDRESS_REJECTED, DESTINATION_NOT_ALLOWED, DESTINATION_LOCKED, CLIENT_UPGRADE_REQUIRED, SESSION_EXPIRED, CANCELED and CONFLICT. Any other error, and a quote timeout, is a failure.

The server uses the counts in three ways:

  • Ranking. After the delivered amount and the ETA, quotes with the same values go in the order of the recent success rate: good starts and succeeded legs, out of all starts and leg outcomes. A pathway's rate is the product of its legs' rates. An adapter with fewer than routing.minSamples (default 20) samples is neutral: it takes the mean rate of the rated adapters in the list. Then the planner's order decides.
  • Circuit breaker. When more than routing.breakerFailureRate (default 0.8) of an adapter's quotes and starts in the current and the previous hour failed, with at least minSamples of them, the breaker opens. POST /sessions/:id/quotes then skips every pathway with that adapter, with no network call, and adds PROVIDER_UNAVAILABLE to errors. After routing.breakerCooldownMinutes (default 10), one request may quote the adapter (half open). A good quote closes the breaker, and the failures from before do not count again. A failed quote opens it for one more cool-down. Turn it off with routing.breaker: false or breakerFailureRate: 1. Metrics: breaker.opened, breaker.closed, quote.skipped.
  • Failover at start. When the first leg's start() throws PROVIDER_UNAVAILABLE or another error with retryable: true, POST /sessions/:id/select starts the next best quote of the same quote request and method from another adapter. The quote must not have expired, and must be inside amountBounds. It must also keep the terms that the user confirmed: the same input amount and asset, the same output asset, and at least 99% of the chosen output. A timeout (an exception, or PROVIDER_UNAVAILABLE with status 504) never fails over, because the provider may have made the order. The server tries at most 2 more quotes. Each try records payment.failover ({ from, to, quoteId }) in the timeline and the metric payment.failover. The answer is the session, with the quote that started in payment.quoteId. A user error (for example AMOUNT_TOO_LOW, KYC_REJECTED or BAD_REQUEST) never fails over. When start() returned any step, the provider has an order, so the server never starts another provider for that payment. When every try fails, the answer is the first error, and the session is as it was before the select.

openramp.admin.providers() and GET /admin/providers show the counts per adapter:

ts
type ProviderHealth = {
  adapterId: string
  windowHours: number
  counts: { quoteOk: number; quoteFail: number; startOk: number; startFail: number; legSucceeded: number; legFailed: number }
  successRate: number | null        // null with fewer than minSamples starts and leg outcomes
  recentFailureRate: number | null  // quotes and starts in the breaker window; null with fewer than minSamples
  breaker: { state: 'closed' | 'open' | 'half_open'; openedAt?: string }
}

CreateSessionInput ​

FieldTypeDefaultDescription
userIdstringrequiredYour user id. Passed to adapters, and in the backend view of the session (session.userId) in webhooks.
externalIdstringnoneYour own id for the session (for example an order id): 1 to 256 printable characters, unique per app. See externalId. It is in the backend view (session.externalId) and the admin views, never in the browser view.
direction'deposit' | 'withdraw''deposit'See Withdrawals
destinationDestinationrequired for a depositWhere the money goes. See below. Withdraw: optional. The app sets the destination at creation: { type: 'crypto', chain, token, address, symbol?, decimals? }, { type: 'fiat', currency } or { type: 'lightning', address } (the same shape as the body of POST /sessions/:id/target). The server checks the format, allowedDestinations and screenAddress. A refused destination throws (400, 403 or 503) and no session is made. Without it, the user picks the destination.
sourceWithdrawSourcerequired for a withdrawal{ chain, token, symbol?, decimals?, custody }: the asset that leaves, and who holds it ('user_wallet' or 'app')
allowedDestinationsAllowedDestinationsany destinationWithdraw only: { crypto?: { chains? }, fiat?: { currencies? }, lightning?: { domains? } }. See Allowed targets.
lockDestinationbooleanfalseWithdraw with destination only (a deposit destination is always locked). Nobody can change the destination: POST /sessions/:id/target answers 409 DESTINATION_LOCKED. The session shows destinationLocked: true, and the modal skips the target screen. See Locked targets.
countrystringnoneISO 3166-1 alpha-2. Picks the currency and local methods.
regionstringnoneISO 3166-2, e.g. US-NY
emailstringnonePrefilled at providers that support it
localestringnoneBCP 47. Picks the modal language; adapters get en when unset.
amountBounds{ min?, max?, currency }noneShown on the amount screen and enforced by the server on what the user pays (see below)
allowedMethodsstring[]allOnly these methods are planned and quoted
metadataRecord<string, string>noneIn the backend view of the session (session.metadata) in every webhook. At most 50 keys. A key has at most 40 characters, a value at most 500.
ttlMinutesnumber30Session lifetime. More than 0 and at most 10080 (7 days).

The server checks the input and throws a 400 (BAD_REQUEST) when a field is not valid:

  • userId: a string of 1 to 256 characters.
  • country: two letters. region: at most 16 characters. email: at most 254. locale: at most 35.
  • amountBounds.min and amountBounds.max: decimal strings, for example "25.50".
  • destination of type crypto: a CAIP-2 chain, a token address or native, and an address that is valid for the chain (an EVM address must not be the zero address). Other types: an ISO 4217 currency.

These checks also apply to the input that authorize returns for POST /sessions.

ts
type Destination =
  | { type: 'crypto'; chain: string; token: string; address: string; symbol?: string; decimals?: number;
      calls?: ContractCall[]; settlement?: { contract: string } }
  | { type: 'merchant'; currency: string; accountRef?: string }
  | { type: 'fiat'; currency: string } // withdraw to cash: set by the server from the user's target

chain is CAIP-2 (eip155:8453), token is an address or native. Pass symbol and decimals so the modal can format amounts without a lookup.

settlement pays through an OpenRampSettlement contract. It needs an EVM chain, an EVM address (the recipient) and an ERC-20 token. calls need settlement, and a call cannot send native value. The server answers 400 when one of these rules fails. See On-chain settlement.

Amount bounds ​

The server checks amountBounds against the input of each quote (what the user pays). It checks only when the bounds currency matches the input: a fiat currency code for fiat inputs, or a token symbol (for example USDC) for crypto inputs. Other inputs pass.

  • POST /sessions/:id/quotes drops a quote outside the bounds and adds AMOUNT_TOO_LOW or AMOUNT_TOO_HIGH (with recovery: 'requote') to errors.
  • POST /sessions/:id/select checks again and answers 422 with the same code.

Provider limits still apply.

sessions.create returns CreatedSession:

ts
type CreatedSession = { id: string; clientSecret: string; expiresAt: string }
type ExistingSession = { id: string; expiresAt: string; existing: true } // a repeated externalId: no client secret

Give clientSecret to the browser. Keep id if you want to look the session up later.

externalId ​

externalId lets your backend send a create again (for example after a timeout) and get the same session. The rules keep a session, and its client secret, with the caller that made it:

The create repeats an externalId ofResult
a session that is not final, with the same userId and the same input (all fields; the server compares a hash){ id, expiresAt, existing: true }. No client secret.
a session of another userId409 EXTERNAL_ID_CONFLICT. No lookup: the answer has no session id, status or secret.
a session with other input (destination, amount bounds, metadata, ...)409 EXTERNAL_ID_CONFLICT
a final session (succeeded, failed, canceled, expired, refunded, reversed)409 EXTERNAL_ID_CONFLICT
  • The server keeps only a hash of each client secret, so it never gives a secret again. Use the secret from the first create (keep it with your order), or make a pay link with openramp.sessions.payLink(id).
  • Two creates at the same time make one session: one gets the client secret, the other gets existing: true.
  • Nothing looks a session up by externalId from the outside: there is no public or admin route for it. Only your server, with the input of the first create, gets the session id back.

verifyWebhook ​

ts
import { verifyWebhook } from '@openrampkit/server'

await verifyWebhook(secret, headers, rawBody, toleranceSec = 300) // Promise<boolean>
generateWebhookSecret()                                            // 'whsec_...': a new Standard Webhooks secret (32 random bytes)
await signWebhook(secret, id, timestamp, body)                     // 'v1,<base64>': the signature the server sends

The webhooks follow Standard Webhooks. verifyWebhook reads the webhook-id, webhook-timestamp and webhook-signature headers. Use it in a service that does not have the openramp instance. See Webhooks to your backend.

WebhookEvent, WebhookEventOf, WebhookEventType, Session and ClientEvent are also exported from @openrampkit/server (they come from @openrampkit/core).

Every response of the handler has the header openramp-version: 1 (the same as apiVersion in webhook events).

Stores ​

ts
import { memoryStore, durableObjectStore, OpenRampStore, cloudflareKvStore, redisStore, fromNodeRedis, fromNodeRedisV4, scopedKV, VersionConflictError } from '@openrampkit/server'
ExportDescription
memoryStore()In memory. Development and tests only.
durableObjectStore(ns, { sessionTtlSec? })Cloudflare Durable Objects. Strongly consistent; the production choice on Workers.
OpenRampStoreThe Durable Object class for durableObjectStore. Export it from your Worker entry.
cloudflareKvStore(ns, { sessionTtlSec? })Workers KV, for demos. Eventually consistent: the version check is best effort.
redisStore(redis, { prefix?, sessionTtlSec? })Redis with an atomic version check (Lua). Works with @upstash/redis directly.
fromNodeRedis(client)Wraps an ioredis client for redisStore
fromNodeRedisV4(client)Wraps a node-redis v4+ client for redisStore
scopedKV(store, prefix)A prefixed key-value view, as adapters get
VersionConflictErrorThrown by put on a version mismatch
SESSION_SCHEMAThe record schema that this server writes in SessionRecord.schema (now 3)
migrateRecord(rec)Brings a stored record up to SESSION_SCHEMA, in place. The server runs it on every store read. See Record schema.
migratingStore(store)store, with migrateRecord on every get. The server wraps config.store with it.

Types: SessionStore, SessionRecord, StoreQueue, QueueItem, TimelineEntry, Resolution, ActiveLeg, StoredQuote, DurableObjectNamespaceLike, DurableObjectStateLike, KVNamespaceLike, RedisLike, NodeRedisLike, NodeRedisV4Like, RedisStoreOptions. See Session stores for the interface and a custom store.

Other exports ​

Types: OpenRampConfig, CreateSessionInput, CreatedSession, OpenRamp, PayLink, SweepResult, TreasuryHook, TreasurySendInput, AdminConfig, Telemetry, AdminListOptions, AdminListResult, AdminSessionSummary, AdminSession, AdminPayment, AdminLeg, AdminOutboxEvent, AdminStats.

isValidAddress(chain, address) checks an address format for a chain: 0x and 40 hex digits (not the zero address) on EVM chains, base58 of 32 to 44 characters on Solana. The /target route uses it.

MIT licensed