# Add session details to account Source: https://docs.rhinestone.dev/api-reference/deposit-service/accounts/add-session-details-to-account https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json post /account/{address}/session Add session details (chain IDs, session digests, and signature) to an existing account # Check if account is registered Source: https://docs.rhinestone.dev/api-reference/deposit-service/accounts/check-if-account-is-registered https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json get /check/{address} Check if an account is registered and return its target chain and token if registered # Fetch or backfill deposit addresses for a registered account Source: https://docs.rhinestone.dev/api-reference/deposit-service/accounts/fetch-or-backfill-deposit-addresses-for-a-registered-account https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json post /account/{address}/deposit-addresses Return the EVM, Solana, and Tron deposit addresses for an already-registered account. Missing Solana and Tron deposit addresses are backfilled on first call (derivation and Rhino.fi mint as needed) without modifying the account config (session details, target). Idempotent on repeat. # Prepare account for registration Source: https://docs.rhinestone.dev/api-reference/deposit-service/accounts/prepare-account-for-registration https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json post /setup-account Derive the Rhinestone smart account for (ownerAddress, sessionOwnerAddress), compare against the existing registration and target, and return either the Solana deposit address (if no re-registration is needed) or the unsigned session details and factory parameters the client must sign before calling /register. # Register a new account Source: https://docs.rhinestone.dev/api-reference/deposit-service/accounts/register-a-new-account https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json post /register Register a new account with factory, factory data, session details, and target. `target.chain` must use a CAIP-2 chain identifier (for example "eip155:8453"). # Register a server-managed account Source: https://docs.rhinestone.dev/api-reference/deposit-service/accounts/register-a-server-managed-account https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json post /register-managed Register a server-owned smart account. Provide a salt for deterministic addressing and a target chain/token. # Operator bulk retry for failed deposits matching a filter Source: https://docs.rhinestone.dev/api-reference/deposit-service/admin/operator-bulk-retry-for-failed-deposits-matching-a-filter https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json post /admin/deposits/retry/bulk Operator-only endpoint. Retry every `failed` deposit for the target identity that matches the supplied filter. Requires the `deposit_retry` API-key scope, at least one of `projectId` / `clientId` (supplying both OR-matches and covers a project's pre- and post-RHI-3857 rows in one call), and at least one narrowing filter (chain, errorCode, failedAfter, failedBefore). # Configure client settings Source: https://docs.rhinestone.dev/api-reference/deposit-service/clients/configure-client-settings https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json post /setup Configure webhook URL, secret, sponsorship rules, and source-token whitelist rules for a client. Deposit whitelist keys must use CAIP-2 chain identifiers (for example "eip155:8453"). # List supported chains and tokens Source: https://docs.rhinestone.dev/api-reference/deposit-service/info/list-supported-chains-and-tokens https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json get /chains Returns all chains supported by the deposit service, with their tokens and whether they support deposits, bridging destinations, or both # Aggregated deposit statistics Source: https://docs.rhinestone.dev/api-reference/deposit-service/processing/aggregated-deposit-statistics https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json get /deposits/stats Returns aggregated deposit statistics (total deposits, unique depositors, and total USD volume) for the authenticated client over the requested rolling time window. Only completed deposits are counted. # List deposits for the authenticated client Source: https://docs.rhinestone.dev/api-reference/deposit-service/processing/list-deposits-for-the-authenticated-client https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json get /deposits Returns the authenticated client's deposits, newest first. Supports filtering by account, status, source chain, and transaction hash. The `account` filter accepts an EVM address or a Solana public key (deposit address or swig address). # Refund a failed deposit Source: https://docs.rhinestone.dev/api-reference/deposit-service/processing/refund-a-failed-deposit https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json post /deposits/refund Transfer the funds from a failed deposit back to a user-provided address on the same chain. # Relay a Safe withdrawal via shared Nexus relayer Source: https://docs.rhinestone.dev/api-reference/deposit-service/processing/relay-a-safe-withdrawal-via-shared-nexus-relayer https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json post /safe/withdraw Relay a signed Safe execTransaction via a shared AWS-owned Nexus account with sponsored gas # Retry failed deposits for an account Source: https://docs.rhinestone.dev/api-reference/deposit-service/processing/retry-failed-deposits-for-an-account https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json post /deposits/retry Retry all failed deposits for a given account address. The API key must belong to the client that registered the account. # Check route liquidity for an amount Source: https://docs.rhinestone.dev/api-reference/deposit-service/utilities/check-route-liquidity-for-an-amount https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json get /liquidity Returns whether the given source→destination route can fulfil the requested amount, along with the underlying liquidity metadata from the orchestrator. # Get a Solana wallet portfolio Source: https://docs.rhinestone.dev/api-reference/deposit-service/utilities/get-a-solana-wallet-portfolio https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json get /portfolio/solana/{address} Returns a normalised SPL portfolio for the given Solana address by paginating through Helius wallet balances. # Get a wallet portfolio Source: https://docs.rhinestone.dev/api-reference/deposit-service/utilities/get-a-wallet-portfolio https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json get /portfolio/{address} Returns a normalised token portfolio for the given address. If the address is a Solana base58 public key, the Solana portfolio is returned; otherwise the EVM portfolio is aggregated from the orchestrator's `/accounts/:address/portfolio` endpoint and priced via the price service. # List persisted webhook events for the calling client Source: https://docs.rhinestone.dev/api-reference/deposit-service/webhooks/list-persisted-webhook-events-for-the-calling-client https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json get /webhooks/events Returns webhook events delivered (or attempted) to the caller's webhook URL. Use for backfill or replay verification. Results are id-desc; pass the last `nextCursor` to paginate. # Re-attempt delivery of a stored webhook event Source: https://docs.rhinestone.dev/api-reference/deposit-service/webhooks/re-attempt-delivery-of-a-stored-webhook-event https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json post /webhooks/events/{id}/resend Re-sends an existing webhook event to the configured webhook URL using its original eventId. Useful for replaying a delivery that failed permanently. Does not allocate a new event id. # Send a test webhook to the configured URL Source: https://docs.rhinestone.dev/api-reference/deposit-service/webhooks/send-a-test-webhook-to-the-configured-url https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/deposit-service.json post /webhooks/test Sends a single webhook with fixture data to the client's configured URL. Uses the client's real webhook secret so signature verification works as in production. Envelope and persisted payload both carry `"test": true` so the receiver can filter and the dashboard can mark it. # Introduction Source: https://docs.rhinestone.dev/api-reference/introduction Reference for the Rhinestone REST APIs. The Rhinestone platform exposes two REST APIs: * **Orchestrator** — quote, submit, and track cross-chain intents. Backs the core Warp flow. * **Deposit Service** — register accounts and process cross-chain deposits with automatic bridging and gas sponsorship. Both share the same base host, `https://v1.orchestrator.rhinestone.dev`, with the Deposit Service mounted at `/deposit-processor`. ## Authentication All endpoints require an API key sent in the `x-api-key` header. The orchestrator additionally supports short-lived JWTs as an alternative for integrators that need bounded credentials or per-request sponsorship policies. * [Get an API key](https://tally.so/r/wg22x4) * [JWT authentication](/intents/configuration/jwt-authentication) ## Errors Errors are returned with standard HTTP status codes and a JSON body. See [Error handling](/intents/guides/error-handling) for the full taxonomy and recovery patterns. ## Guides If you're integrating from scratch, start with the workflow guides — they show how the endpoints fit together end-to-end. Build the full intent flow: quote, sign, submit, track. Register accounts and accept cross-chain deposits. # Create Intent Source: https://docs.rhinestone.dev/api-reference/orchestrator/intents/create-intent https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/orchestrator.json post /intents Submits a quoted intent for execution. Takes the `intentId` from `POST /quotes` (`routes[].intentId`) plus signatures (origin, destination, optionally target-execution) and optional EIP-7702 authorizations. # Get Intent Source: https://docs.rhinestone.dev/api-reference/orchestrator/intents/get-intent https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/orchestrator.json get /intents/{id} Retrieves the status of an intent along with per-claim progress across chains. # Split Intent by Liquidity Source: https://docs.rhinestone.dev/api-reference/orchestrator/intents/split-intent-by-liquidity https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/orchestrator.json post /intents/splits Splits token amounts into multiple intents based on available relayer liquidity. Each returned intent can be filled by a single relayer. # Get Account Portfolio Source: https://docs.rhinestone.dev/api-reference/orchestrator/portfolio/get-account-portfolio https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/orchestrator.json get /accounts/{accountAddress}/portfolio Retrieves the token balances across all supported chains for a user's abstracted account # Create Quote Source: https://docs.rhinestone.dev/api-reference/orchestrator/quotes/create-quote https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/orchestrator.json post /quotes Computes a quote for a cross-chain intent: token transfers, target executions, and cost breakdown. Returns a pre-ranked `routes[]` with per-route `intentId` to pass to `POST /intents` for execution. # Route liquidity limit Source: https://docs.rhinestone.dev/api-reference/orchestrator/utilities/route-liquidity-limit https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/orchestrator.json get /liquidity Returns the maximum fillable amount for a single deposit on the given source→destination route. # Supported chains and tokens Source: https://docs.rhinestone.dev/api-reference/orchestrator/utilities/supported-chains-and-tokens https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/orchestrator.json get /chains Get supported chains and tokens with additional metadata # Account registration Source: https://docs.rhinestone.dev/deposits/api/account-registration Register managed or user-owned smart accounts for cross-chain deposit processing. Every depositing user needs a registered smart account. You can let the service create one (managed) or bring your own (user-owned). ## Choose an account type | | Managed account | User-owned account | | ----------------------- | ----------------------------------------------- | ------------------------------------------------------- | | Who creates the account | Deposit service | You, via SDK | | Session key setup | Automatic | You sign session authorization | | User-facing address | Service returns deposit address | Your existing smart account address | | Recipient | Required (you specify where funds go) | Optional (defaults to the account itself) | | Best for | Existing apps, EOA users (e.g. browser wallets) | Non-custodial flows, users with existing smart accounts | Managed accounts are recommended for most integrations — simpler setup, no SDK dependency for registration. User-owned accounts are for cases where deposits should go to an existing smart account the user already controls. All examples below use these shared constants: ```ts theme={null} const DEPOSIT_SERVICE_URL = "https://v1.orchestrator.rhinestone.dev/deposit-processor"; const API_KEY = "YOUR_RHINESTONE_API_KEY"; const headers = { "Content-Type": "application/json", "x-api-key": API_KEY, }; ``` ## Register an account The service creates a Nexus smart account deterministically from your API key and a salt you provide. The same API key + salt always produces the same deposit address, so you can safely re-register if needed. Use a stable, unique identifier per user — for example, an internal user ID. Hash it for privacy: ```ts theme={null} import { keccak256, toHex } from "viem"; const salt = keccak256(toHex("user-123")); ``` ```ts theme={null} const response = await fetch(`${DEPOSIT_SERVICE_URL}/register-managed`, { method: "POST", headers, body: JSON.stringify({ account: { salt, target: { chain: "eip155:42161", // Arbitrum token: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC recipient: "0xYOUR_RECIPIENT_ADDRESS", }, }, }), }); const { evmDepositAddress, solanaDepositAddress } = await response.json(); ``` The response includes two deposit addresses: | Field | Description | | ---------------------- | ------------------------------------------- | | `evmDepositAddress` | Accepts deposits on any supported EVM chain | | `solanaDepositAddress` | Accepts deposits on Solana | Both addresses route to the same target chain and token. ```ts theme={null} const check = await fetch(`${DEPOSIT_SERVICE_URL}/check/${evmDepositAddress}`); const data = await check.json(); ``` ```json theme={null} { "isRegistered": true, "targetChain": "eip155:42161", "targetToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "sourceChains": ["eip155:8453", "eip155:10", "eip155:42161"] } ``` For this flow, you create a Rhinestone smart account via the SDK, configure session keys so the deposit service can sign bridging transactions, and register the account with its factory data and session details. ```bash theme={null} npm install @rhinestone/sdk viem ``` ```ts theme={null} import { RhinestoneSDK, type RhinestoneAccountConfig } from "@rhinestone/sdk"; const rhinestone = new RhinestoneSDK(); const config: RhinestoneAccountConfig = { owners: { type: "ecdsa", accounts: [userSigner], }, experimental_sessions: { enabled: true }, }; const account = await rhinestone.createAccount(config); const address = account.getAddress(); const { factory, factoryData } = account.getInitData(); ``` The deposit service uses a session key to sign bridging transactions on the user's behalf. You need to build session details for every chain the user can deposit from, plus the target chain. ```ts theme={null} import { toViewOnlyAccount } from "@rhinestone/sdk/utils"; import { base, optimism, arbitrum } from "viem/chains"; const RHINESTONE_SIGNER_ADDRESS = "0x177bfcdd15bc01e99013dcc5d2b09cd87a18ce9c"; const sessionSigner = toViewOnlyAccount(RHINESTONE_SIGNER_ADDRESS); const sourceChains = [base, optimism, arbitrum]; const sessions = sourceChains.map((chain) => ({ owners: { type: "ecdsa" as const, accounts: [sessionSigner] }, chain, })); const sessionDetails = await account.experimental_getSessionDetails(sessions); const signature = await account.experimental_signEnableSession(sessionDetails); const enableSessionDetails = { hashesAndChainIds: sessionDetails.hashesAndChainIds, signature, }; ``` ```ts theme={null} const response = await fetch(`${DEPOSIT_SERVICE_URL}/register`, { method: "POST", headers, body: JSON.stringify( { account: { address, accountParams: { factory, factoryData, sessionDetails: enableSessionDetails, }, target: { chain: "eip155:42161", // Arbitrum token: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC }, }, }, (_, v) => (typeof v === "bigint" ? v.toString() : v), ), }); const { evmDepositAddress, solanaDepositAddress } = await response.json(); ``` The JSON replacer `(_, v) => typeof v === "bigint" ? v.toString() : v` is needed because `sessionDetails.hashesAndChainIds` contains `bigint` chain IDs that `JSON.stringify` can't serialize by default. ```ts theme={null} const check = await fetch(`${DEPOSIT_SERVICE_URL}/check/${address}`); const data = await check.json(); // { isRegistered: true, targetChain: "eip155:42161", ... } ``` ### Adding source chains To accept deposits from chains that weren't included in the original registration, add new session details: ```ts theme={null} import { polygon } from "viem/chains"; const newSessions = [ { owners: { type: "ecdsa" as const, accounts: [sessionSigner] }, chain: polygon, }, ]; const newSessionDetails = await account.experimental_getSessionDetails(newSessions); const newSignature = await account.experimental_signEnableSession(newSessionDetails); await fetch(`${DEPOSIT_SERVICE_URL}/account/${address}/session`, { method: "POST", headers, body: JSON.stringify( { sessionDetails: { hashesAndChainIds: newSessionDetails.hashesAndChainIds, signature: newSignature, }, }, (_, v) => (typeof v === "bigint" ? v.toString() : v), ), }); ``` Managed accounts automatically support all available source chains — this step is only needed for user-owned accounts. ## Optional: token routing By default, all deposits are bridged to the single target token you set at registration. Token routing rules let you select the output token based on what the user deposited. When registering an account, the `target` object accepts two optional fields: | Field | Type | Description | | ------------------ | ------------- | ---------------------------------------------------------------------------------------------------- | | `outputTokenRules` | `Array` | Routing rules evaluated by match specificity | | `rejectUnmapped` | `boolean` | If `true`, deposits that don't match any rule are rejected instead of falling back to `target.token` | Each rule has: | Field | Type | Description | | -------------- | ------------------ | ---------------------------------------------------------------- | | `match.chain` | `string` (CAIP-2) | Match deposits from this source chain (e.g. `"eip155:1"`) | | `match.token` | `string` (address) | Match deposits of this source token address | | `match.symbol` | `string` | Match deposits by token symbol (case-insensitive, e.g. `"USDC"`) | | `outputToken` | `string` (address) | The final token to deliver on the target chain | A rule's `match` must specify at least one of `chain`, `token`, or `symbol`. You can combine them for more specific matches. ### Rule priority When multiple rules match a deposit, the most specific rule wins. Declaration order only matters when two rules share the same specificity. | Match type | Example | | ------------------ | -------------------------------------------------------------------- | | `chain` + `token` | Specific token from a specific chain | | `chain` + `symbol` | Any token with symbol X from chain Y | | `token` (only) | Specific token from any chain | | `symbol` (only) | Any token with symbol X from any chain | | `chain` (only) | Any token from a specific chain | | No match | Falls back to `target.token` (or rejected if `rejectUnmapped: true`) | ### Example: USDC and ETH passthrough Route USDC deposits to USDC.e and ETH deposits to WETH on Optimism, while defaulting other tokens to a fallback: ```ts theme={null} await fetch(`${DEPOSIT_SERVICE_URL}/register-managed`, { method: "POST", headers, body: JSON.stringify({ account: { salt, target: { chain: "eip155:10", // Optimism token: "0xYOUR_DEFAULT_FALLBACK_TOKEN", recipient: "0xYOUR_RECIPIENT_ADDRESS", outputTokenRules: [ { match: { symbol: "USDC" }, outputToken: "0x7f5c764cbc14f9669b88837ca1490cca17c31607", // USDC.e on Optimism }, { match: { symbol: "ETH" }, outputToken: "0x4200000000000000000000000000000000000006", // WETH on Optimism }, ], }, }, }), }); ``` With this configuration: * User deposits USDC (from any chain) → receives USDC.e on Optimism * User deposits ETH (from any chain) → receives WETH on Optimism * User deposits any other token → receives `target.token` (default fallback) ### Example: chain-specific overrides Combine `chain` and `symbol` for chain-specific routing. The chain-specific rule takes priority because `chain + symbol` outranks `symbol` alone. ```ts theme={null} outputTokenRules: [ { match: { chain: "eip155:1", symbol: "USDC" }, outputToken: "0xUSCD_BRIDGED_ADDRESS", }, { match: { symbol: "USDC" }, outputToken: "0xUSDC_NATIVE_ADDRESS", }, ]; ``` ### Example: reject unknown tokens Only accept specific tokens and reject everything else: ```ts theme={null} await fetch(`${DEPOSIT_SERVICE_URL}/register-managed`, { method: "POST", headers, body: JSON.stringify({ account: { salt, target: { chain: "eip155:10", token: "0xNOT_USED_AS_FALLBACK", recipient: "0xYOUR_RECIPIENT_ADDRESS", outputTokenRules: [ { match: { symbol: "USDC" }, outputToken: "0xUSDC_ADDRESS" }, { match: { symbol: "ETH" }, outputToken: "0xWETH_ADDRESS" }, ], rejectUnmapped: true, }, }, }), }); ``` Deposits that don't match USDC or ETH are ignored (not bridged). ## Optional: configure post-bridge actions Post-bridge actions execute after the bridged tokens arrive on the target chain — for example, swapping into a protocol-specific token. Set `postBridgeActions` in the `target` object at registration time. When present, `target.token` is the intermediate token bridged into the account, and the last action's `outputToken` is the final token delivered to the recipient. ```ts theme={null} await fetch(`${DEPOSIT_SERVICE_URL}/register-managed`, { method: "POST", headers, body: JSON.stringify({ account: { salt, target: { chain: "eip155:8453", token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC bridged first recipient: "0xYOUR_RECIPIENT_ADDRESS", postBridgeActions: [ { type: "orderbook-swap", contract: "0xSWAP_CONTRACT_ADDRESS", outputToken: "0xFINAL_TOKEN_ADDRESS", }, ], }, }, }), }); ``` | Field | Type | Description | | ------------- | ------------------ | -------------------------------------- | | `type` | `"orderbook-swap"` | The action type | | `contract` | `Address` | Swap contract address | | `outputToken` | `Address` | Final token delivered to the recipient | # Deposit processing Source: https://docs.rhinestone.dev/deposits/api/deposit-processing How deposits are detected, bridged, and settled — plus status tracking, retries, and error handling. Once an account is registered, the deposit service handles everything from detection to settlement. This page explains what happens at each stage and how to monitor the process. ## Deposit lifecycle ```mermaid actions={false} theme={null} flowchart LR D[Detection] --> B[Bridging] B -- success --> S[Settlement] B -- failure --> R[Retry] R --> B ``` ### Detection The service monitors registered accounts via chain indexer webhooks. When a token transfer is detected on a registered account, it goes through validation: * The token and amount are checked against your [deposit whitelist](/deposits/api/initial-setup#restrict-accepted-deposits) (if configured) * The transfer is deduplicated by chain, transaction hash, account, and token * If valid, the deposit enters the pipeline with status `processing` ### Bridging The service submits a bridging intent to the Rhinestone Orchestrator, which routes the funds through the optimal settlement layer (Across, Relay, or others). If the primary layer fails, the service tries alternatives automatically. ### Settlement Tokens arrive on the target chain in the registered target token. If a recipient address was set at registration, funds are forwarded there. The service marks the deposit as `completed` and sends a `bridge-complete` [webhook](/deposits/api/status-tracking#webhooks). ## Deposit statuses | Status | Meaning | | ------------ | ---------------------------------------------- | | `processing` | Deposit detected, bridging in progress | | `completed` | Funds arrived on target chain | | `failed` | Bridging failed — may be retried automatically | ## Retries ### Automatic Transient errors — bridge failures, session activation issues, and internal errors — are retried automatically. Configuration errors like unsupported tokens, insufficient balance, or unregistered accounts require manual resolution. ### Manual Force an immediate retry of all failed deposits for an account: ```ts theme={null} const response = await fetch(`${DEPOSIT_SERVICE_URL}/deposits/retry`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, body: JSON.stringify({ address: accountAddress, }), }); const { deposits } = await response.json(); // [{ txHash: "0x...", chain: "eip155:8453" }, ...] ``` ## Error codes When a deposit fails, the error code indicates what went wrong and whether the service will retry automatically. ### Retryable These are transient failures. The service retries automatically, or you can trigger a [manual retry](#manual). | Code | Description | | ------------ | ---------------------------------- | | `SESSION-2` | Session key activation failed | | `BRIDGE-1` | Bridge submission failed | | `BRIDGE-2` | No bridge quote available | | `BRIDGE-3` | Bridge timed out | | `BRIDGE-4` | Bridge provider failed | | `BRIDGE-5` | Price deviation exceeded tolerance | | `BRIDGE-6` | Unsupported bridge route | | `SWAP-1` | Post-bridge swap failed | | `INTERNAL-1` | Unexpected service error | ### Non-retryable These indicate a configuration or input problem. Check account registration, deposit whitelist, and token support. | Code | Description | | ----------- | -------------------------------------- | | `BALANCE-1` | Account balance too low for bridging | | `BALANCE-3` | Deposit amount outside whitelist range | | `TOKEN-1` | Token not supported on this chain | | `TOKEN-3` | Token not in deposit whitelist | | `ACCOUNT-1` | Account not registered | Failed deposits with non-retryable errors will not be retried automatically. Resolve the underlying issue (e.g. update your deposit whitelist or register the account) before triggering a manual retry. # Fees Source: https://docs.rhinestone.dev/deposits/api/fees Understand the fee structure for deposits. Every deposit processed through the Deposit API may incur fees depending on the source chain, target chain, and tokens involved. Fees are deducted from the deposit amount — the user receives the deposited amount minus any applicable fees. ## Fee types | Fee | Applies when | Covers | | ------------ | ------------------------------ | ---------------------------------------------------- | | **Gas** | Always | On-chain execution costs on source and target chains | | **Bridging** | Source and target chain differ | Settlement layer fee (Across, Relay, etc.) | | **Swap** | Source and target token differ | DEX execution fee for token conversion | A single deposit can incur multiple fee types. For example, depositing ETH on Arbitrum to receive USDC on Base incurs all three: gas for on-chain execution, a bridging fee for the cross-chain settlement, and a swap fee for the ETH → USDC conversion. Same-chain deposits skip the bridging fee, and same-token deposits skip the swap fee. ## How fees are applied Fees are subtracted from the deposit amount during processing. The user receives the remainder on the target chain. For example, if a user deposits 100 USDC and total fees are 0.50 USDC, the user receives 99.50 USDC on the target chain. To cover fees on behalf of your users so they receive the full amount, see [sponsorship](/deposits/api/sponsorship). ## Learn more * [Fees](/home/resources/fees) — general Rhinestone fee structure and current rates * [Sponsorship](/deposits/api/sponsorship) — cover fees on behalf of your users * [Initial setup](/deposits/api/initial-setup) — configure deposit whitelists and price deviation tolerance # Initial setup Source: https://docs.rhinestone.dev/deposits/api/initial-setup Configure webhooks, fee sponsorship, and deposit whitelists for your Deposit API client. Call `POST /setup` to configure how the deposit service handles your deposits. All fields are optional — configure what you need. Each call performs a partial update; omitted fields keep their current value. All examples below use these shared constants: ```ts theme={null} const DEPOSIT_SERVICE_URL = "https://v1.orchestrator.rhinestone.dev/deposit-processor"; const API_KEY = "YOUR_RHINESTONE_API_KEY"; const headers = { "Content-Type": "application/json", "x-api-key": API_KEY, }; ``` ## Configure a webhook Register a URL to receive notifications about deposit events. Optionally provide a secret for HMAC-SHA256 signature verification. ```ts theme={null} await fetch(`${DEPOSIT_SERVICE_URL}/setup`, { method: "POST", headers, body: JSON.stringify({ params: { webhookUrl: "https://your-domain.com/notify", webhookSecret: "your-secret-key", }, }), }); ``` When a `webhookSecret` is set, every webhook request includes an `X-Webhook-Signature` header you can verify. See [status tracking](/deposits/api/status-tracking#webhooks) for event types, payload format, and verification examples. Once your endpoint is live, fire a single fixture event with [`POST /webhooks/test`](/api-reference/deposit-service/webhooks/send-a-test-webhook-to-the-configured-url) to confirm signature handling and parsing before real deposits arrive. The envelope carries `"test": true` so receivers can filter it. ## Sponsor fees Cover gas, bridging, and swap fees on behalf of your users so they receive the full deposit amount. Sponsorship is configured per source chain using [CAIP-2](https://chainagnostic.org/CAIPs/caip-2) identifiers. Chains without explicit config default to no sponsorship. ```ts theme={null} await fetch(`${DEPOSIT_SERVICE_URL}/setup`, { method: "POST", headers, body: JSON.stringify({ params: { sponsorship: { "eip155:8453": { gas: "all", bridging: "all" }, // Base "eip155:10": { gas: "all" }, // Optimism "eip155:42161": { gas: "deployed" }, // Arbitrum }, }, }), }); ``` See [sponsorship](/deposits/api/sponsorship) for the full list of fee types, available values, and how sponsorship is resolved at processing time. ## Restrict accepted deposits Define a whitelist of accepted tokens per source chain. Deposits of unlisted tokens are silently ignored. You can also set minimum and maximum deposit amounts per token. ```ts theme={null} await fetch(`${DEPOSIT_SERVICE_URL}/setup`, { method: "POST", headers, body: JSON.stringify({ params: { depositWhitelist: { "eip155:8453": [ { token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base minAmount: "1000000", // 1 USDC minimum maxAmount: "5000000000", // 5,000 USDC maximum }, ], "eip155:42161": [ { token: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" }, // USDC on Arbitrum, no limits { token: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", minAmount: "1000000", }, // USDT, $1 min ], }, }, }), }); ``` | Field | Type | Description | | ----------- | -------- | ---------------------------------------------------------------- | | `token` | `string` | Token address (`0x`-prefixed) | | `minAmount` | `string` | Optional. Minimum accepted amount in raw token units (inclusive) | | `maxAmount` | `string` | Optional. Maximum accepted amount in raw token units (inclusive) | If no whitelist is set, all [supported tokens](/deposits/overview#supported-chains-and-tokens) are accepted with no amount restrictions. Rejected deposits trigger a `bridge-failed` webhook with error code `TOKEN-3` (token not allowed) or `BALANCE-3` (amount out of range). ## Set price deviation tolerance For stablecoin-to-stablecoin bridges, set the maximum allowed price deviation in basis points. Deposits that exceed this threshold are rejected with error code `BRIDGE-5`. ```ts theme={null} await fetch(`${DEPOSIT_SERVICE_URL}/setup`, { method: "POST", headers, body: JSON.stringify({ params: { maxPriceDeviationBps: 100, // 1% max deviation }, }), }); ``` The default is `200` (2%) if not set. Tighten this for high-value stablecoin corridors; loosen it if you see rejections during volatile periods. ## Clear a configuration field Each `/setup` call is a partial update. To explicitly clear a field, pass `null` for string fields or `{}` for object fields: | To clear | Pass | | ---------------------- | ------ | | `webhookUrl` | `null` | | `webhookSecret` | `null` | | `sponsorship` | `{}` | | `depositWhitelist` | `{}` | | `maxPriceDeviationBps` | `null` | ## Put it all together A single `/setup` call configuring everything at once: ```ts theme={null} await fetch(`${DEPOSIT_SERVICE_URL}/setup`, { method: "POST", headers, body: JSON.stringify({ params: { webhookUrl: "https://your-domain.com/notify", webhookSecret: "your-secret-key", sponsorship: { "eip155:8453": { gas: "all", bridging: "all" }, "eip155:10": { gas: "all" }, }, depositWhitelist: { "eip155:8453": [ { token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", minAmount: "1000000", }, ], "eip155:42161": [ { token: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" }, ], }, maxPriceDeviationBps: 150, }, }), }); ``` # Quickstart Source: https://docs.rhinestone.dev/deposits/api/quickstart Set up the Deposit API and process your first cross-chain deposit. Register a managed account, deposit USDC on one chain, and receive it on another — all through a few API calls. ## Prerequisites * A Rhinestone API key * A wallet with testnet USDC on Optimism Sepolia ([Circle faucet](https://faucet.circle.com/)) Set up gas sponsorship so deposit bridging is covered. This is a one-time call per API key. ```ts theme={null} const DEPOSIT_SERVICE_URL = "https://v1.orchestrator.rhinestone.dev/deposit-processor"; const API_KEY = "YOUR_RHINESTONE_API_KEY"; const response = await fetch(`${DEPOSIT_SERVICE_URL}/setup`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, body: JSON.stringify({ params: { sponsorship: { "eip155:84532": { gas: "all" }, // Base Sepolia "eip155:11155420": { gas: "all" }, // Optimism Sepolia }, }, }), }); console.log(`Setup: ${response.status}`); ``` You should see `Setup: 200`. Register a server-managed account with a target chain and token. The service creates a smart account deterministically from your API key and the salt you provide, and returns deposit addresses. ```ts theme={null} import { keccak256, toHex } from "viem"; // Use any unique identifier per user (e.g., internal user ID) const salt = keccak256(toHex("user-123")); const RECIPIENT = "0xYOUR_RECIPIENT_ADDRESS"; const TARGET_CHAIN = "eip155:84532"; // Base Sepolia const TARGET_TOKEN = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; // USDC on Base Sepolia const registerResponse = await fetch( `${DEPOSIT_SERVICE_URL}/register-managed`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, body: JSON.stringify({ account: { salt, target: { chain: TARGET_CHAIN, token: TARGET_TOKEN, recipient: RECIPIENT, }, }, }), }, ); const { evmDepositAddress, solanaDepositAddress } = await registerResponse.json(); console.log(`EVM deposit address: ${evmDepositAddress}`); console.log(`Solana deposit address: ${solanaDepositAddress}`); ``` You should see two deposit addresses printed. The `evmDepositAddress` is where users send tokens on any supported EVM chain. ```ts theme={null} const check = await fetch(`${DEPOSIT_SERVICE_URL}/check/${evmDepositAddress}`); const checkData = await check.json(); console.log(checkData); ``` You should see: ```json theme={null} { "isRegistered": true, "targetChain": "eip155:84532", "targetToken": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "sourceChains": ["eip155:84532", "eip155:11155420", "eip155:421614"] } ``` Transfer USDC to the deposit address on Optimism Sepolia. The deposit service detects it and bridges it to Base Sepolia automatically. ```ts theme={null} import { createWalletClient, http, encodeFunctionData, erc20Abi, parseUnits, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { optimismSepolia } from "viem/chains"; const funder = privateKeyToAccount("0xYOUR_FUNDING_KEY"); const walletClient = createWalletClient({ account: funder, chain: optimismSepolia, transport: http(), }); // USDC on Optimism Sepolia const USDC = "0x5fd84259d66Cd46123540766Be93DFE6D43130D7"; const txHash = await walletClient.sendTransaction({ to: USDC, data: encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [evmDepositAddress, parseUnits("1", 6)], }), }); console.log(`Deposit tx: ${txHash}`); ``` Poll the deposits endpoint by transaction hash to track the bridging progress. See [status tracking](/deposits/api/status-tracking#polling) for the full response schema. ```ts theme={null} async function waitForDeposit(txHash: string) { const url = `${DEPOSIT_SERVICE_URL}/deposits?txHash=${txHash}`; while (true) { const response = await fetch(url, { headers: { "x-api-key": API_KEY }, }); const { deposits } = await response.json(); const deposit = deposits[0]; if (deposit?.status === "completed") { console.log("Deposit completed:", deposit.destinationTxHash); return deposit; } if (deposit?.status === "failed") { console.error("Deposit failed:", deposit.errorCode); return deposit; } await new Promise((r) => setTimeout(r, 1_000)); } } await waitForDeposit(txHash); ``` Once bridging completes, the deposit status changes to `completed` and includes the destination transaction hash. The USDC is now at the recipient address on Base Sepolia. ## Next steps Webhooks, sponsorship rules, and deposit whitelists. User-owned accounts, session configuration, and output token rules. Deposit lifecycle, retries, and status tracking. Track deposits via polling or webhooks. # Sponsorship Source: https://docs.rhinestone.dev/deposits/api/sponsorship Sponsor deposit fees on behalf of your users. Sponsorship lets you absorb deposit fees so your users receive the full deposited amount on the target chain. You configure which fee types to sponsor, independently per source chain, via the [`/setup` endpoint](/deposits/api/initial-setup#sponsor-fees). ## Sponsorable fees Each fee type is controlled independently: | Fee | Values | Description | | ------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Gas** | `"all"`, `"deployed"`, `"none"` | `"all"` sponsors gas for every deposit. `"deployed"` only sponsors gas when the account is already deployed on the source chain — useful to avoid covering first-time deployment costs. Default: `"none"`. | | **Bridging** | `"all"`, `"none"` | Covers the settlement layer bridging fee. Default: `"none"`. | | **Swap** | `"all"`, `"none"` | Covers the DEX swap fee when a token conversion is needed. Default: `"none"`. | ## Per-chain configuration Sponsorship is configured per source chain using [CAIP-2](https://chainagnostic.org/CAIPs/caip-2) identifiers. Each chain is configured independently — there's no global toggle. Chains without explicit config default to no sponsorship. ```ts theme={null} await fetch(`${DEPOSIT_SERVICE_URL}/setup`, { method: "POST", headers, body: JSON.stringify({ params: { sponsorship: { // Base: fully sponsored "eip155:8453": { gas: "all", bridging: "all", swap: "all" }, // Optimism: only gas "eip155:10": { gas: "all" }, // Arbitrum: gas for deployed accounts only "eip155:42161": { gas: "deployed" }, // Ethereum, Polygon: no sponsorship (not listed) }, }, }), }); ``` ## How sponsorship is resolved Each fee type is resolved independently when processing a deposit, based on the source chain config and the account's deployment status at that moment. * Each fee type is resolved independently. You can sponsor bridging without sponsoring gas, or vice versa. * Deployment status is checked at processing time, not at registration. An account registered before deployment will start receiving gas sponsorship once it's deployed (if the mode is `"deployed"`). * Chains without explicit config default to no sponsorship. Omitting a fee type from the chain config is equivalent to `"none"`. ## Effect on deposit amount **Sponsored fees** are covered by your sponsorship balance. The user receives the full bridged amount — if they deposit 100 USDC and fees are 0.50 USDC, they receive the full 100 USDC on the target chain. **Unsponsored fees** are deducted from the deposit. The user receives less than they sent — if they deposit 100 USDC and fees are 0.50 USDC, they receive 99.50 USDC. ## Learn more * [Initial setup](/deposits/api/initial-setup#sponsor-fees) — configure sponsorship via `/setup` * [Gas & fee sponsorship](/smart-wallet/gas-sponsorship/overview) — sponsorship in the Smart Wallet SDK context * [Fees](/deposits/api/fees) — full fee type breakdown # Status tracking Source: https://docs.rhinestone.dev/deposits/api/status-tracking Track deposit status via polling or webhooks — two approaches for monitoring the deposit lifecycle. There are two ways to track a deposit through the processing pipeline: * **Polling** — query the `GET /deposits` endpoint filtered by transaction hash. Simple and stateless. * **Webhooks** — receive push notifications as deposits move through each stage. Real-time and event-driven. Use whichever fits your architecture. Many integrations combine both: webhooks for real-time updates, polling as a fallback or for on-demand status checks. ## Polling Query the `GET /deposits` endpoint with the `txHash` parameter to look up a deposit by its source transaction hash. ```ts theme={null} const DEPOSIT_SERVICE_URL = "https://v1.orchestrator.rhinestone.dev/deposit-processor"; const API_KEY = "YOUR_RHINESTONE_API_KEY"; const txHash = "0xabc123..."; const response = await fetch( `${DEPOSIT_SERVICE_URL}/deposits?txHash=${txHash}`, { headers: { "x-api-key": API_KEY }, }, ); const { deposits } = await response.json(); ``` ### Response Each item in the `deposits` array has the following shape: | Field | Type | Description | | ------------------- | ---------------- | -------------------------------------------------------------------------------- | | `chain` | `string` | Source chain (CAIP-2) | | `txHash` | `string` | Source transaction hash | | `token` | `string` | Deposit token address | | `amount` | `string` | Deposit amount (raw token units) | | `sender` | `string` | Sender address | | `account` | `string` | Registered account address | | `targetChain` | `string` | Destination chain (CAIP-2) | | `targetToken` | `string` | Destination token address | | `status` | `string` | `"processing"`, `"completed"`, or `"failed"` | | `sourceTxHash` | `string \| null` | Bridge source transaction hash | | `destinationTxHash` | `string \| null` | Bridge destination transaction hash | | `sourceAmount` | `string \| null` | Bridge source amount | | `destinationAmount` | `string \| null` | Bridge destination amount | | `errorCode` | `string \| null` | [Error code](/deposits/api/deposit-processing#error-codes) if the deposit failed | | `createdAt` | `string` | ISO 8601 timestamp of when the deposit was detected | | `completedAt` | `string \| null` | ISO 8601 timestamp of when the deposit completed | ### Polling loop Poll until the deposit reaches a terminal status (`completed` or `failed`): ```ts theme={null} async function waitForDeposit(txHash: string): Promise { const url = `${DEPOSIT_SERVICE_URL}/deposits?txHash=${txHash}`; const headers = { "x-api-key": API_KEY }; while (true) { const response = await fetch(url, { headers }); const { deposits } = await response.json(); const deposit = deposits[0]; if (!deposit) { // Deposit not yet indexed — wait and retry await new Promise((r) => setTimeout(r, 1_000)); continue; } if (deposit.status === "completed") { console.log("Deposit completed:", deposit.destinationTxHash); return; } if (deposit.status === "failed") { console.error("Deposit failed:", deposit.errorCode); return; } // Still processing — poll again await new Promise((r) => setTimeout(r, 1_000)); } } ``` A 1-second interval works well for most use cases. Most deposits complete within seconds. ## Webhooks The deposit service sends webhook notifications to your configured endpoint as deposits move through the processing pipeline. All webhooks are `POST` requests with `Content-Type: application/json`. Configure your webhook URL and optional secret via the [`POST /setup`](/deposits/api/initial-setup#configure-a-webhook) endpoint. ### Payload envelope Every webhook request body follows the same envelope structure: ```json theme={null} { "version": "1.0", "type": "", "eventId": "12345", "time": "2025-01-15T12:00:00.000Z", "data": { ... } } ``` | Field | Type | Description | | --------- | ---------------------- | ----------------------------------------------------------------------- | | `version` | `string` | Protocol version. Currently `"1.0"` | | `type` | `string` | Event type identifier | | `eventId` | `string` | Stable identifier for this delivery. Use it as the canonical dedupe key | | `time` | `string` | ISO 8601 timestamp of when the event was sent | | `test` | `boolean \| undefined` | Present and `true` only for events dispatched via the test endpoint | | `data` | `object` | Event-specific payload (see below) | ### Event types | Type | Trigger | | --------------------------------------------------------- | ----------------------------------------------------------------------------- | | [`deposit-received`](#deposit-received) | Token transfer detected on a registered account | | [`bridge-started`](#bridge-started) | Bridging intent created and submitted to the Orchestrator | | [`bridge-complete`](#bridge-complete) | Tokens arrived on the target chain | | [`bridge-delayed`](#bridge-delayed) | Bridge provider did not fill within the expected window; a refund is expected | | [`bridge-failed`](#bridge-failed) | Bridging failed | | [`post-bridge-swap-complete`](#post-bridge-swap-complete) | Post-bridge token swap completed | | [`post-bridge-swap-failed`](#post-bridge-swap-failed) | Post-bridge token swap failed | | [`deposit-refunded`](#deposit-refunded) | Deposit funds returned to a recipient on the source chain | #### `deposit-received` Sent when an incoming token transfer is detected on a registered account. | Field | Type | Description | | ----------------- | -------- | ------------------------------------------- | | `chain` | `string` | Source chain (CAIP-2, e.g. `"eip155:8453"`) | | `token` | `string` | Token address | | `amount` | `string` | Deposit amount in raw token units | | `account` | `string` | Account address | | `transactionHash` | `string` | Deposit transaction hash | | `sender` | `string` | Sender address | ```json theme={null} { "version": "1.0", "type": "deposit-received", "eventId": "12345", "time": "2025-01-15T12:00:00.000Z", "data": { "chain": "eip155:8453", "token": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "amount": "1000000", "account": "0x1234567890abcdef1234567890abcdef12345678", "transactionHash": "0xabc123...", "sender": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" } } ``` #### `bridge-started` Sent when a bridging intent is created and submitted to the Orchestrator. | Field | Type | Description | | --------------------------- | --------------------- | --------------------------------------------------------------- | | `source.chain` | `string` | Source chain (CAIP-2) | | `source.asset` | `string` | Source asset address | | `source.amount` | `string` | Source amount | | `destination.chain` | `string` | Destination chain (CAIP-2) | | `destination.asset` | `string` | Destination asset address | | `destination.amount` | `string` | Destination amount | | `account` | `string` | Account address | | `deposit.transactionHash` | `string` | Original deposit transaction hash | | `deposit.chain` | `string` | Deposit chain (CAIP-2) | | `deposit.asset` | `string` | Deposit asset address | | `deposit.amount` | `string` | Deposit amount | | `deposit.sender` | `string` | Sender address | | `settlementLayer` | `string` | Settlement layer used (e.g. `"layerzero"`) | | `estimatedFillTime.seconds` | `number \| undefined` | Estimated time to fill, in seconds, when the route provides one | #### `bridge-complete` Sent when tokens have arrived on the target chain. | Field | Type | Description | | ----------------------------- | --------------------- | ----------------------------------- | | `deposit.transactionHash` | `string` | Original deposit transaction hash | | `deposit.chain` | `string` | Deposit chain (CAIP-2) | | `deposit.asset` | `string` | Deposit asset address | | `deposit.amount` | `string` | Deposit amount | | `deposit.sender` | `string` | Sender address | | `source.transactionHash` | `string` | Source chain claim transaction hash | | `source.chain` | `string` | Source chain (CAIP-2) | | `source.amount` | `string` | Source amount | | `source.asset` | `string` | Source asset address | | `destination.transactionHash` | `string` | Destination chain transaction hash | | `destination.chain` | `string` | Destination chain (CAIP-2) | | `destination.amount` | `string` | Destination amount | | `destination.asset` | `string` | Destination asset address | | `account` | `string` | Account address | | `settlementLayer` | `string \| undefined` | Settlement layer used | ```json theme={null} { "version": "1.0", "type": "bridge-complete", "eventId": "12346", "time": "2025-01-15T12:01:30.000Z", "data": { "deposit": { "transactionHash": "0xabc123...", "chain": "eip155:8453", "asset": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "amount": "1000000", "sender": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" }, "source": { "transactionHash": "0xdef456...", "chain": "eip155:8453", "amount": "1000000", "asset": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" }, "destination": { "transactionHash": "0x789ghi...", "chain": "eip155:42161", "amount": "990000", "asset": "0xaf88d065e77c8cc2239327c5edb3a432268e5831" }, "account": "0x1234567890abcdef1234567890abcdef12345678" } } ``` #### `bridge-delayed` Sent when the bridge provider has not filled the intent within the expected window. A refund is expected on the source chain; the subsequent [`deposit-refunded`](#deposit-refunded) event confirms the funds returned. | Field | Type | Description | | ---------------------------- | --------------------- | -------------------------------------------------------------- | | `reason` | `string` | Human-readable explanation of the delay | | `estimatedDelayTime.seconds` | `number \| undefined` | Additional time the refund is expected to take, when available | | `account` | `string` | Account address | | `deposit.transactionHash` | `string` | Deposit transaction hash | | `deposit.chain` | `string` | Deposit chain (CAIP-2) | | `deposit.token` | `string` | Deposit token address | | `deposit.amount` | `string` | Deposit amount | | `deposit.sender` | `string` | Sender address | #### `bridge-failed` Sent when a bridging operation fails. See [error codes](/deposits/api/deposit-processing#error-codes) for the full list and retry behavior. | Field | Type | Description | | ------------------------- | --------------------- | -------------------------------- | | `errorCode` | `string` | Error code (e.g. `"BRIDGE-1"`) | | `account` | `string` | Account address | | `message` | `string \| undefined` | Human-readable error description | | `deposit.transactionHash` | `string` | Deposit transaction hash | | `deposit.chain` | `string` | Deposit chain (CAIP-2) | | `deposit.token` | `string` | Deposit token address | | `deposit.amount` | `string` | Deposit amount | | `deposit.sender` | `string` | Sender address | #### `post-bridge-swap-complete` Sent when a post-bridge token swap completes successfully. This occurs when the account has [token routing rules](/deposits/api/account-registration) configured that require a swap after bridging. | Field | Type | Description | | ------------------------- | --------------------- | -------------------------------------- | | `deposit.transactionHash` | `string` | Original deposit transaction hash | | `deposit.chain` | `string` | Deposit chain (CAIP-2) | | `deposit.asset` | `string` | Deposit asset address | | `deposit.amount` | `string` | Deposit amount | | `deposit.sender` | `string` | Sender address | | `swap.transactionHash` | `string` | Swap transaction hash | | `swap.chain` | `string` | Chain where the swap executed (CAIP-2) | | `swap.tokenIn` | `string` | Input token address | | `swap.tokenOut` | `string` | Output token address | | `swap.amount` | `string` | Output amount | | `swap.recipient` | `string` | Recipient address | | `bridge.transactionHash` | `string \| undefined` | Bridge transaction hash | | `bridge.chain` | `string \| undefined` | Bridge chain (CAIP-2) | | `bridge.asset` | `string \| undefined` | Bridge asset address | | `bridge.amount` | `string \| undefined` | Bridge amount | | `account` | `string` | Account address | #### `post-bridge-swap-failed` Sent when a post-bridge token swap fails. | Field | Type | Description | | ---------------------------- | --------------------- | ------------------------------------------- | | `errorCode` | `string` | Error code (e.g. `"SWAP-1"`) | | `message` | `string \| undefined` | Human-readable error description | | `account` | `string` | Account address | | `deposit.transactionHash` | `string` | Original deposit transaction hash | | `deposit.chain` | `string` | Deposit chain (CAIP-2) | | `deposit.asset` | `string` | Deposit asset address | | `deposit.amount` | `string` | Deposit amount | | `deposit.sender` | `string` | Sender address | | `swap.chain` | `string` | Chain where the swap was attempted (CAIP-2) | | `swap.tokenIn` | `string` | Input token address | | `swap.tokenOut` | `string` | Output token address | | `swap.amount` | `string` | Attempted swap amount | | `swap.recipient` | `string` | Intended recipient address | | `swap.bridgeTransactionHash` | `string \| undefined` | Bridge transaction hash | #### `deposit-refunded` Sent when funds from a deposit are returned to a recipient on the source chain. Typically follows a [`bridge-delayed`](#bridge-delayed) event. | Field | Type | Description | | ------------------------- | -------- | --------------------------------- | | `account` | `string` | Account address | | `deposit.transactionHash` | `string` | Original deposit transaction hash | | `deposit.chain` | `string` | Deposit chain (CAIP-2) | | `deposit.asset` | `string` | Deposit asset address | | `deposit.amount` | `string` | Deposit amount | | `deposit.sender` | `string` | Sender address | | `refund.transactionHash` | `string` | Refund transaction hash | | `refund.chain` | `string` | Refund chain (CAIP-2) | | `refund.asset` | `string` | Refund asset address | | `refund.amount` | `string` | Refund amount | | `refund.recipient` | `string` | Address that received the refund | ### Signature verification If you provided a `webhookSecret` during [setup](/deposits/api/initial-setup#configure-a-webhook), every webhook request includes an `X-Webhook-Signature` header: ``` X-Webhook-Signature: sha256= ``` The signature is an HMAC-SHA256 hash computed over the raw JSON request body using your secret. To verify: 1. Read the raw request body as a string (before JSON parsing) 2. Compute the HMAC-SHA256 of the raw body using your webhook secret 3. Compare the result with the value in the `X-Webhook-Signature` header (strip the `sha256=` prefix) 4. Use a constant-time comparison to prevent timing attacks ```ts theme={null} import { createHmac, timingSafeEqual } from "node:crypto"; function verifyWebhookSignature( rawBody: string, signatureHeader: string, secret: string, ): boolean { const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex"); return ( signatureHeader.length === expected.length && timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected)) ); } ``` Always verify against the **raw request body** string, not a re-serialized version of the parsed JSON. Re-serialization may change key order or whitespace, which will produce a different signature. ### Delivery behavior * **Retries** — failed deliveries are retried multiple times before the event is marked `failed`. Events that exhaust their retries can still be replayed on demand via [`POST /webhooks/events/{id}/resend`](/api-reference/deposit-service/webhooks/re-attempt-delivery-of-a-stored-webhook-event), which reuses the original `eventId`. * **Ordering** — events for a single deposit are sent in lifecycle order (`deposit-received` → `bridge-started` → `bridge-complete`), but there is no global ordering guarantee across deposits. * **URL validation** — the webhook URL must use HTTPS and must not target internal or private network addresses. * **Idempotency** — use `eventId` from the envelope as the canonical dedupe key. ### Backfilling missed events If your receiver was offline or rejected events, replay them through the events API. Every dispatched webhook is persisted regardless of delivery outcome. List events delivered (or attempted) to your URL, newest first: ```ts theme={null} const response = await fetch( `${DEPOSIT_SERVICE_URL}/webhooks/events?since=2025-01-15T00:00:00Z`, { headers: { "x-api-key": API_KEY } }, ); const { events, nextCursor } = await response.json(); ``` Re-send a stored event to your URL — the original `eventId` is reused so dedupe still holds: ```ts theme={null} await fetch(`${DEPOSIT_SERVICE_URL}/webhooks/events/${eventId}/resend`, { method: "POST", headers: { "x-api-key": API_KEY }, }); ``` For more details, see the API reference for [`GET /webhooks/events`](/api-reference/deposit-service/webhooks/list-persisted-webhook-events-for-the-calling-client) and [`POST /webhooks/events/{id}/resend`](/api-reference/deposit-service/webhooks/re-attempt-delivery-of-a-stored-webhook-event). ## Conventions * EVM addresses and token addresses are **lowercase**. Non-EVM addresses (Solana, Tron) preserve their original case. * All amounts are **strings** (raw token units, not human-readable). * Chains use [CAIP-2](https://chainagnostic.org/CAIPs/caip-2) identifiers (e.g. `"eip155:8453"` for Base). # Demo Source: https://docs.rhinestone.dev/deposits/demo # Overview Source: https://docs.rhinestone.dev/deposits/overview Accept deposits from any chain into your app with Rhinestone's cross-chain deposit infrastructure. Rhinestone Deposits is a cross-chain deposit infrastructure that lets you accept tokens from users on any supported chain and deliver them to a target chain and token automatically. You don't need to build bridging logic, manage gas across chains, or handle token swaps — the service detects deposits, bridges them via [Warp](/home/introduction/rhinestone-intents), and notifies your app when funds arrive. It's built for teams that need reliable deposit rails: neobanks, modern dapps, DeFi protocols, or any app that onboards users from multiple chains. It supports a wide range of EVM chains and Solana — see [supported chains and tokens](#supported-chains-and-tokens) below. ## Two ways to integrate A headless backend service for programmatic deposit handling. You register accounts, configure webhooks, and process deposits server-side. Full control over the deposit flow. A drop-in React modal that handles wallet connection, chain selection, and deposit execution out of the box. Minimal frontend effort for a polished deposit experience. ## How it works ```mermaid actions={false} theme={null} sequenceDiagram participant App as Your app participant DS as Deposit Service participant Warp as Warp (Orchestrator) participant Dest as Target chain App->>DS: Register account with target chain/token Note over App: User sends tokens on any source chain DS->>DS: Detect deposit via webhook listener DS->>Warp: Create bridging intent Warp->>Dest: Route and settle funds DS->>App: Webhook notification (bridge-complete) ``` 1. You register a smart account with a target chain and token 2. The user transfers tokens to their smart account on any supported source chain 3. The deposit service detects the transfer, creates a bridging intent via Warp, and routes the funds to the target chain 4. Your app receives a webhook notification when the deposit completes The user makes a single transfer. Everything else — bridging, swaps, gas — is handled automatically. ## Why Deposits Deposits are self-custodial by design — funds are held in the user's smart account at every step, and Rhinestone never takes custody. Stablecoin swaps settle at parity, fees can be fully sponsored, and destination calls let funds land directly into a vault or position. The result is a deposit rail that feels like a native single-chain transfer, without the trust trade-offs of a centralized bridge. Under the hood, Rhinestone aggregates multiple bridging providers, solvers, and quoting services, routing each deposit through the best available path. If one provider is degraded or a route is unavailable, the service falls back automatically — giving you a single integration with the reliability of several. ## Key features * **Automatic bridging** — deposits are detected and bridged to the target chain without any user interaction beyond the initial transfer * **Multi-chain support** — accept deposits from Ethereum, Base, Arbitrum, Optimism, Polygon, and Solana, with more chains added regularly * **1:1 stablecoin swaps** — USDC and USDT are swapped at parity * **Swap routing** — route between tokens as part of the deposit * **Calls on destination** — trigger contract calls on the target chain once funds arrive * **Fee sponsorship** — cover gas, bridging, and swap fees on a per-chain basis ## User experience From the user's perspective, depositing is a simple token transfer — send tokens to an address on any supported chain. There's no bridging UI, no gas token management, and no chain switching. With the **widget**, the experience is even more streamlined: the user connects their wallet, selects a chain and token, and confirms the deposit — all within a single modal. The widget also supports withdrawals. With the **API**, you control the UX entirely. The user interacts with your app however you design it, and the deposit service handles everything behind the scenes. ## Supported chains and tokens Rhinestone Deposits supports a wide range of EVM chains plus Solana. Each chain is enabled as a **source** (the user can send funds to it), a **destination** (funds can settle on it), or both. * **Source** — users can transfer tokens on this chain and have them bridged to the target. * **Destination** — accounts on this chain can be registered as the target where funds land. * **Tokens** — `All` means any token routable through Warp; otherwise, only the listed tokens are accepted. ### Mainnet | Name | Source | Destination | Tokens | | :-------------- | :----: | :---------: | :-------------- | | Ethereum | ✓ | ✓ | All | | OP Mainnet | ✓ | ✓ | All | | BNB Smart Chain | ✓ | ✓ | All | | Gnosis | — | ✓ | All | | Polygon | ✓ | ✓ | All | | Sonic | — | ✓ | All | | HyperEVM | — | ✓ | USDC, USDT0 | | Soneium | ✓ | ✓ | ETH, USDC, WETH | | Base | ✓ | ✓ | All | | Plasma | ✓ | ✓ | All | | Arbitrum One | ✓ | ✓ | All | | Solana | ✓ | — | USDC, USDT, SOL | ### Testnet | Name | Source | Destination | Tokens | | :--------------- | :----: | :---------: | :----------------------- | | Sepolia | ✓ | ✓ | ETH, USDC, WETH | | OP Sepolia | ✓ | ✓ | ETH, USDC, WETH | | Base Sepolia | ✓ | ✓ | ETH, USDC, WETH, MockUSD | | Arbitrum Sepolia | ✓ | ✓ | ETH, USDC, WETH | | Plasma Testnet | — | ✓ | USDT0, USDC | This data is also available programmatically via the [List supported chains and tokens](/api-reference/deposit-service/info/list-supported-chains-and-tokens) endpoint. ## Which should you use? | | Deposit API | Deposit Widget | | ------------------ | ------------------------------------ | --------------------------------------- | | Integration effort | Moderate — backend + webhook handler | Low — drop in a React component | | UI control | Full — build your own | Themed modal with customization options | | Deposit triggers | Any transfer to the smart account | User-initiated via the modal | | Withdrawal support | Manual | Built-in withdraw modal | # Customization Source: https://docs.rhinestone.dev/deposits/widget/customization Theme, brand, and configure the deposit widget to match your app. The deposit and withdraw modals accept `theme`, `branding`, and `uiConfig` props to control appearance and behavior. ## Theme Pass a `theme` object to control the modal's visual style. ```tsx theme={null} ``` ### Properties | Property | Type | Default | Description | | ----------------- | -------------------------------------------------- | --------- | -------------------------------- | | `mode` | `"light"` \| `"dark"` | `"light"` | Color mode | | `radius` | `"none"` \| `"sm"` \| `"md"` \| `"lg"` \| `"full"` | `"md"` | Border radius preset | | `fontColor` | `string` | — | Primary text color | | `iconColor` | `string` | — | Icon color | | `ctaColor` | `string` | — | Call-to-action button background | | `ctaHoverColor` | `string` | — | CTA button hover background | | `borderColor` | `string` | — | Border color | | `backgroundColor` | `string` | — | Modal background color | All color values accept any CSS color string (hex, rgb, hsl, etc.). Omitted properties use the design system defaults for the selected `mode`. ## Branding Replace the modal header logo and title with your own. ```tsx theme={null} ``` ### Properties | Property | Type | Default | Description | | --------- | -------- | -------------------------- | ------------------------------ | | `logoUrl` | `string` | Rhinestone logo | Image URL for the modal header | | `title` | `string` | `"Deposit"` / `"Withdraw"` | Header title text | ## UI configuration Toggle UI elements and set deposit constraints via `uiConfig`. ```tsx theme={null} ``` ### Properties | Property | Type | Default | Description | | ---------------- | ------------------------------------ | ------- | -------------------------------------------------------------------------------------------------- | | `showLogo` | `boolean` | `false` | Show the branding logo in the header | | `showStepper` | `boolean` | `false` | Show the progress stepper | | `showBackButton` | `boolean` | `true` | Show the back button in the header | | `balance` | `{ title: string, amount?: string }` | — | Custom balance display. `title` sets the label, `amount` optionally overrides the displayed value. | | `maxDepositUsd` | `number` | — | Maximum deposit amount in USD | | `minDepositUsd` | `number` | — | Minimum deposit amount in USD | # Deposit modal Source: https://docs.rhinestone.dev/deposits/widget/deposit-modal Configure the deposit modal for different wallet connection modes and deposit flows. The `DepositModal` component handles the full deposit flow: wallet connection, source chain and token selection, amount input, and cross-chain bridging. It supports three wallet integration modes depending on how your app manages wallets. ## recipient vs dappAddress Two props look similar but serve different purposes: * **`recipient`** (required) — the address that receives the bridged funds on the target chain. This is a pure delivery target. * **`dappAddress`** (embedded wallet & QR modes) — the address that owns the deposit. The modal uses it to derive the smart account and, by default, as the delivery target. It is the user's identity in the flow, not a destination. In external wallet mode, the connected wallet is the owner, so only `recipient` is needed. In embedded and QR modes, you must pass `dappAddress` because the modal has no other way to know who owns the deposit. ## Integration modes ### External wallet The user connects their own wallet via Reown (WalletConnect). The modal manages the wallet connection UI internally. Use this when your app doesn't have existing wallet infrastructure. Pass a `reownAppId` to enable external wallet connection. ```tsx theme={null} import { DepositModal } from "@rhinestone/deposit-modal"; import "@rhinestone/deposit-modal/styles.css"; setIsOpen(false)} targetChain={8453} targetToken="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" recipient="0xYOUR_RECIPIENT_ADDRESS" reownAppId="YOUR_REOWN_PROJECT_ID" onDepositComplete={(data) => console.log(data)} /> ``` ### Embedded wallet Your app manages the wallet (e.g. via Privy, Dynamic, or Turnkey) and passes the owner address to the modal. The modal skips its own wallet connection UI and uses your app's wallet context. Pass `dappAddress` with the wallet owner address, and `onRequestConnect` to trigger your app's login flow when the modal needs a connected wallet. ```tsx theme={null} import { DepositModal } from "@rhinestone/deposit-modal"; import "@rhinestone/deposit-modal/styles.css"; setIsOpen(false)} targetChain={8453} targetToken="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" recipient="0xYOUR_RECIPIENT_ADDRESS" dappAddress={embeddedWalletAddress} onRequestConnect={() => login()} onDepositComplete={(data) => console.log(data)} /> ``` ### QR code deposit The modal displays a deposit address and QR code. The user sends funds from any wallet externally — no wallet connection is required inside the modal. Pass `dappAddress` without `onRequestConnect` or `reownAppId`. ```tsx theme={null} import { DepositModal } from "@rhinestone/deposit-modal"; import "@rhinestone/deposit-modal/styles.css"; setIsOpen(false)} targetChain={8453} targetToken="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" recipient="0xYOUR_RECIPIENT_ADDRESS" dappAddress={ownerAddress} onDepositComplete={(data) => console.log(data)} /> ``` ## Transfer configuration Control the deposit destination and optionally pre-fill source parameters. | Prop | Type | Required | Description | | ------------------ | ------------------- | -------- | ------------------------------------------------------------------------------------------ | | `targetChain` | `Chain \| number` | Yes | Destination chain (viem `Chain` object or chain ID) | | `targetToken` | `Address` | Yes | Token address on the destination chain | | `recipient` | `Address` | Yes | Where funds are delivered on the target chain | | `defaultAmount` | `string` | No | Pre-filled deposit amount | | `sourceChain` | `Chain \| number` | No | Pre-selected source chain | | `sourceToken` | `Address` | No | Pre-selected source token | | `allowedRoutes` | `RouteConfig` | No | Restrict available source chains and tokens | | `outputTokenRules` | `OutputTokenRule[]` | No | Route the deposit to a different final token based on what the user deposited | | `rejectUnmapped` | `boolean` | No | Reject deposits that don't match any routing rule instead of falling back to `targetToken` | The `allowedRoutes` prop accepts `{ sourceChains?: number[], sourceTokens?: string[] }` to limit what the user can select. For supported chains and tokens, see [supported chains](/deposits/overview#supported-chains-and-tokens). ## Session configuration Sessions allow the widget to execute bridging on behalf of the user's smart account. These props control session behavior. | Prop | Type | Default | Description | | ----------------- | ---------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `sessionChainIds` | `number[]` | All supported chains | Restrict which chains get session keys | | `forceRegister` | `boolean` | `false` | Force session re-creation even if the account is already registered | | `waitForFinalTx` | `boolean` | `true` | Wait for destination chain confirmation before firing `onDepositComplete`. When `false`, resolves after the bridge is submitted. | ## Destination token routing Deliver a different final token depending on what the user deposits. Pass `outputTokenRules` to map source deposits — matched by chain, token address, or symbol — to the output token delivered on the target chain. Deposits that don't match any rule fall back to `targetToken`, or are rejected when `rejectUnmapped` is `true`. ```tsx theme={null} ``` When several rules match the same deposit, the most specific one wins: `chain + token` outranks `chain + symbol`, which outranks `token`, then `symbol`, then `chain` alone. See [token routing](/deposits/api/account-registration#optional-token-routing) for the full rule semantics, priority order, and additional examples. ## Post-bridge actions Execute actions on the destination chain after bridging completes. Use this for automated swaps into vault tokens or other DeFi positions. ```tsx theme={null} ``` The flow becomes: deposit on source chain → bridge to target chain → swap into the output token → deliver to the recipient. ## Display modes By default, the component renders as a centered modal overlay with a backdrop. Set `inline={true}` to render it without the overlay, fitting into your page layout. ```tsx theme={null} {}} inline={true} // ...other props /> ``` Set `closeOnOverlayClick={false}` to prevent the modal from closing when the user clicks outside it. ## Props reference ### Required | Prop | Type | Description | | ------------- | ----------------- | --------------------------------------------------- | | `isOpen` | `boolean` | Controls modal visibility | | `onClose` | `() => void` | Called when the user closes the modal | | `targetChain` | `Chain \| number` | Destination chain (viem `Chain` object or chain ID) | | `targetToken` | `Address` | Token address on the destination chain | | `recipient` | `Address` | Where funds are delivered on the target chain | ### Wallet | Prop | Type | Default | Description | | -------------------- | -------------- | ------- | -------------------------------------------------------- | | `reownAppId` | `string` | — | Reown project ID. Enables external wallet connection. | | `dappAddress` | `Address` | — | Owner address for embedded wallet or QR code flows | | `dappWalletClient` | `WalletClient` | — | Host-provided viem wallet client for signing | | `dappPublicClient` | `PublicClient` | — | Host-provided viem public client | | `onRequestConnect` | `() => void` | — | Called when the modal needs the user to connect a wallet | | `connectButtonLabel` | `string` | — | Custom label for the connect button | ### Transfer | Prop | Type | Default | Description | | ------------------- | -------------------- | ------- | ------------------------------------------------------------- | | `defaultAmount` | `string` | — | Pre-filled deposit amount | | `sourceChain` | `Chain \| number` | — | Pre-selected source chain | | `sourceToken` | `Address` | — | Pre-selected source token | | `allowedRoutes` | `RouteConfig` | — | `{ sourceChains?, sourceTokens? }` to restrict selection | | `outputTokenRules` | `OutputTokenRule[]` | — | Per-deposit output token routing rules | | `rejectUnmapped` | `boolean` | `false` | Reject deposits that don't match any `outputTokenRules` entry | | `postBridgeActions` | `PostBridgeAction[]` | — | Actions to execute after bridging | ### Session | Prop | Type | Default | Description | | ------------------ | ---------- | -------------- | --------------------------------------- | | `sessionChainIds` | `number[]` | All supported | Chain IDs for session key creation | | `forceRegister` | `boolean` | `false` | Force session re-creation | | `waitForFinalTx` | `boolean` | `true` | Wait for destination chain confirmation | | `signerAddress` | `Address` | Default signer | Session signer address | | `rhinestoneApiKey` | `string` | — | API key for account setup | ### Backend | Prop | Type | Default | Description | | ------------ | -------- | ------------------------- | ------------------------------------------- | | `backendUrl` | `string` | Rhinestone production URL | URL of your deposit-widget-backend instance | ### Display | Prop | Type | Default | Description | | --------------------- | ---------------------- | ------- | ------------------------------------------------------------------- | | `inline` | `boolean` | `false` | Render without modal overlay | | `closeOnOverlayClick` | `boolean` | `true` | Close modal on backdrop click | | `className` | `string` | — | CSS class for the modal container | | `theme` | `DepositModalTheme` | — | [Theme configuration](/deposits/widget/customization#theme) | | `branding` | `DepositModalBranding` | — | [Branding configuration](/deposits/widget/customization#branding) | | `uiConfig` | `DepositModalUIConfig` | — | [UI configuration](/deposits/widget/customization#ui-configuration) | | `debug` | `boolean` | `false` | Enable debug logging | ### Callbacks | Prop | Type | Description | | -------------------- | ------------------------------------------- | ------------------------------------------------------------- | | `onReady` | `() => void` | Modal initialized | | `onConnected` | `(data: ConnectedEventData) => void` | Smart account created | | `onDepositSubmitted` | `(data: DepositSubmittedEventData) => void` | Deposit transaction submitted on source chain | | `onDepositComplete` | `(data: DepositCompleteEventData) => void` | Tokens arrived on target chain | | `onDepositFailed` | `(data: DepositFailedEventData) => void` | Bridge or transfer failed | | `onError` | `(data: ErrorEventData) => void` | Error at any stage | | `onEvent` | `(event: DepositEvent) => void` | [Analytics event](/deposits/widget/status-tracking#analytics) | See [status tracking](/deposits/widget/status-tracking) for callback payload types and the deposit lifecycle. ### Solana | Prop | Type | Default | Description | | -------------- | --------- | -------------- | ----------------------------------------- | | `enableSolana` | `boolean` | `true` | Enable Solana wallet support and QR flows | | `solanaRpcUrl` | `string` | Solana mainnet | Custom Solana RPC endpoint | # Quickstart Source: https://docs.rhinestone.dev/deposits/widget/quickstart Add the deposit widget to your React app and accept cross-chain deposits in minutes. Install the `@rhinestone/deposit-modal` package, render the modal, and handle a completed deposit. This quickstart uses **external wallet mode** with Reown for wallet connection. If you don't want to set up Reown, the modal also supports [QR code deposits](/deposits/widget/deposit-modal#qr-code-deposit) (no wallet connection) and [embedded wallets](/deposits/widget/deposit-modal#embedded-wallet) (Privy, Dynamic, Turnkey, etc.). ## Prerequisites * A React 18+ app (Next.js, Vite, or similar) * A [Rhinestone API key](https://tally.so/r/wg22x4) * A [Reown](https://cloud.reown.com/) project ID (for wallet connection) ## Install ```bash theme={null} npm install @rhinestone/deposit-modal viem wagmi @tanstack/react-query @reown/appkit @reown/appkit-adapter-wagmi @solana/web3.js @solana/spl-token ``` `@solana/web3.js` and `@solana/spl-token` are required even for EVM-only apps because Solana support is enabled by default. To drop these dependencies, pass `enableSolana={false}` on the modal. The modal ships with its own `wagmi` and `@tanstack/react-query` providers — you do not need to wrap your app with `WagmiProvider` or `QueryClientProvider`. Import the modal and its styles. Pass the target chain, token, and your Reown project ID. ```tsx theme={null} import { useState } from "react"; import { DepositModal } from "@rhinestone/deposit-modal"; import "@rhinestone/deposit-modal/styles.css"; function App() { const [isOpen, setIsOpen] = useState(false); return ( <> setIsOpen(false)} targetChain={8453} targetToken="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" recipient="0xYOUR_RECIPIENT_ADDRESS" reownAppId={process.env.NEXT_PUBLIC_REOWN_PROJECT_ID!} /> ); } ``` This renders a modal where the user connects their wallet, picks a source chain and token, enters an amount, and confirms the deposit. Bridging to the target chain is handled automatically. Add the `onDepositComplete` callback to react when tokens arrive on the target chain. ```tsx {8-11} theme={null} setIsOpen(false)} targetChain={8453} targetToken="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" recipient="0xYOUR_RECIPIENT_ADDRESS" reownAppId={process.env.NEXT_PUBLIC_REOWN_PROJECT_ID!} onDepositComplete={(data) => { console.log("Deposit complete:", data.destinationTxHash); setIsOpen(false); }} /> ``` See [`onDepositComplete`](/deposits/widget/status-tracking#ondepositcomplete) for the full `data` payload shape, and [status tracking](/deposits/widget/status-tracking) for all available callbacks. ## Next steps Integration modes, transfer configuration, and full props reference. Theme, branding, and UI configuration. Lifecycle events, callbacks, and error handling. Withdraw tokens from a Safe to any supported chain. # Status tracking Source: https://docs.rhinestone.dev/deposits/widget/status-tracking Track deposit progress and react to state changes using widget callbacks. The deposit modal fires callbacks at each stage of the deposit lifecycle. Use these to update your UI, trigger backend processes, or log analytics. ## Deposit lifecycle ```mermaid actions={false} theme={null} sequenceDiagram participant App as Your app participant Modal as Deposit modal participant Chain as Blockchain Modal->>App: onReady Note over Modal: User connects wallet Modal->>App: onConnected Note over Modal: User selects chain, token, amount Modal->>Chain: Submit deposit tx Modal->>App: onDepositSubmitted Note over Chain: Bridge in progress Chain-->>Modal: Funds arrive on target chain Modal->>App: onDepositComplete ``` 1. The modal initializes and fires `onReady` 2. The user connects a wallet (or is connected via embedded wallet). The modal creates a smart account and fires `onConnected` with the EOA `address` and `smartAccount` address. 3. The user selects a source chain, token, and amount, then confirms the deposit 4. The modal submits the transaction on the source chain and fires `onDepositSubmitted` 5. The bridge routes funds to the target chain. When `waitForFinalTx` is `true` (default), the modal waits for destination chain confirmation before firing `onDepositComplete` If the bridge fails after submission, `onDepositFailed` fires instead of `onDepositComplete`. ## Event callbacks ### onReady Fires when the modal is initialized and ready for interaction. No payload. ```tsx theme={null} onReady={() => { console.log("Modal ready"); }} ``` ### onConnected Fires when a smart account has been created for the deposit. ```tsx theme={null} onConnected={(data) => { console.log("EOA:", data.address); console.log("Smart account:", data.smartAccount); }} ``` | Field | Type | Description | | -------------- | --------- | ------------------------------------------------ | | `address` | `Address` | The user's EOA or owner address | | `smartAccount` | `Address` | The smart account address that receives deposits | ### onDepositSubmitted Fires when the user signs and submits the deposit transaction on the source chain. ```tsx theme={null} onDepositSubmitted={(data) => { console.log("Submitted:", data.txHash, "on chain", data.sourceChain); }} ``` | Field | Type | Description | | ------------- | -------------------- | ------------------------------------------- | | `txHash` | `string` | Source chain transaction hash | | `sourceChain` | `number \| "solana"` | Source chain ID | | `amount` | `string` | Deposit amount in the token's smallest unit | ### onDepositComplete Fires when tokens arrive on the target chain (or when the bridge is submitted, if `waitForFinalTx` is `false`). ```tsx theme={null} onDepositComplete={(data) => { console.log("Completed:", data.destinationTxHash); console.log("Amount:", data.amount); }} ``` | Field | Type | Description | | ------------------- | --------------------- | ----------------------------------------------------------------------- | | `txHash` | `string` | Source chain transaction hash | | `destinationTxHash` | `string \| undefined` | Target chain transaction hash. Absent when `waitForFinalTx` is `false`. | | `amount` | `string` | Amount transferred | | `sourceChain` | `number \| "solana"` | Source chain ID | | `sourceToken` | `string` | Source token address | | `targetChain` | `number` | Target chain ID | | `targetToken` | `string` | Target token address | ### onDepositFailed Fires when the bridge fails after the deposit was submitted. ```tsx theme={null} onDepositFailed={(data) => { console.log("Failed:", data.txHash, data.error); }} ``` | Field | Type | Description | | -------- | --------------------- | ----------------------------- | | `txHash` | `string` | Source chain transaction hash | | `error` | `string \| undefined` | Error message, if available | ### onError Fires on errors at any stage — wallet connection, transaction signing, bridge setup, etc. ```tsx theme={null} onError={(data) => { console.error(`[${data.code}] ${data.message}`); }} ``` | Field | Type | Description | | --------- | --------------------- | ------------------------ | | `message` | `string` | Error description | | `code` | `string \| undefined` | Error code, if available | For bridge-level error codes, see [deposit processing error codes](/deposits/api/deposit-processing#error-codes). ## Error handling Errors can occur at different stages: | Stage | Callback | Typical causes | | ------------------- | ----------------- | ------------------------------------------- | | Wallet connection | `onError` | User rejected connection, network error | | Transaction signing | `onError` | User rejected transaction, insufficient gas | | After submission | `onDepositFailed` | Bridge failure, timeout, price deviation | | Any stage | `onError` | Unexpected errors | `onError` fires for errors that prevent the deposit from being submitted. `onDepositFailed` fires for failures that happen after the source chain transaction is confirmed — at that point, the deposit service may [retry automatically](/deposits/api/deposit-processing#retries). ## Analytics The `onEvent` callback fires on granular user interactions for integration with your analytics pipeline. ```tsx theme={null} onEvent={(event) => { analytics.track(event.type, event); }} ``` Events include modal views (`*_open`) and CTA clicks (`*_cta_click`) at each step of the flow, with contextual properties like selected token, chain, and amount. # Withdraw modal Source: https://docs.rhinestone.dev/deposits/widget/withdraw-modal Let users withdraw tokens from a Safe to any supported chain using the withdraw modal. The withdraw modal currently only supports 1/1 Safe wallets. The `WithdrawModal` component handles outbound transfers from a Safe. The user selects a destination chain and token, enters an amount, and the modal constructs and signs the withdrawal transaction. ## Basic usage ```tsx theme={null} import { WithdrawModal } from "@rhinestone/deposit-modal"; import "@rhinestone/deposit-modal/styles.css"; setIsOpen(false)} safeAddress="0xYOUR_SAFE_ADDRESS" sourceChain={8453} sourceToken="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" targetChain={10} targetToken="0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" reownAppId="YOUR_REOWN_PROJECT_ID" onWithdrawComplete={(data) => { console.log("Withdrawal complete:", data.destinationTxHash); }} /> ``` ## Transaction signing By default, the modal uses Reown for wallet connection and signing. If your app manages wallets directly (e.g. Privy embedded wallets), pass `onSignTransaction` to handle Safe transaction signing yourself. The callback receives a `SafeTransactionRequest` containing the `safeTxHash` and EIP-712 typed data. Return a signature. ```tsx theme={null} { const provider = await getEmbeddedWalletProvider(); // Sign the Safe transaction hash const signature = await provider.request({ method: "personal_sign", params: [request.safeTxHash, embeddedWalletAddress], }); // Adjust v value for Safe's eth_sign verification const v = parseInt(signature.slice(-2), 16); const adjusted = signature.slice(0, -2) + (v + 4).toString(16); return { signature: adjusted }; }} /> ``` Safe's `eth_sign` path requires adding 4 to the `v` value of a `personal_sign` signature. This adjustment is specific to Safe's signature verification — see the [Safe docs](https://docs.safe.global/advanced/smart-account-signatures#eth_sign-signature) for details. ## Props reference ### Required | Prop | Type | Description | | ------------- | ----------------- | ----------------------------------------- | | `isOpen` | `boolean` | Controls modal visibility | | `onClose` | `() => void` | Called when the user closes the modal | | `safeAddress` | `Address` | The 1/1 Safe contract address | | `sourceChain` | `Chain \| number` | Chain where the Safe holds funds | | `sourceToken` | `Address` | Token to withdraw from the Safe | | `targetChain` | `Chain \| number` | Destination chain | | `targetToken` | `Address` | Token to receive on the destination chain | ### Wallet | Prop | Type | Default | Description | | -------------------- | ------------------------------------------------------------------ | ------- | --------------------------------------- | | `reownAppId` | `string` | — | Reown project ID for wallet connection | | `dappAddress` | `Address` | — | Owner address for embedded wallet flows | | `dappWalletClient` | `WalletClient` | — | Host-provided viem wallet client | | `dappPublicClient` | `PublicClient` | — | Host-provided viem public client | | `onSignTransaction` | `(request: SafeTransactionRequest) => Promise<{ signature: Hex }>` | — | Custom Safe transaction signer | | `onRequestConnect` | `() => void` | — | Called when wallet connection needed | | `connectButtonLabel` | `string` | — | Custom connect button label | ### Transfer | Prop | Type | Default | Description | | --------------- | ------------- | --------------------- | -------------------------------- | | `recipient` | `Address` | Smart account address | Delivery address on target chain | | `defaultAmount` | `string` | — | Pre-filled withdrawal amount | | `allowedRoutes` | `RouteConfig` | — | Restrict available routes | ### Session | Prop | Type | Default | Description | | ------------------ | ---------- | -------------- | --------------------------------------- | | `sessionChainIds` | `number[]` | All supported | Chain IDs for session key creation | | `forceRegister` | `boolean` | `false` | Force session re-creation | | `waitForFinalTx` | `boolean` | `true` | Wait for destination chain confirmation | | `signerAddress` | `Address` | Default signer | Session signer address | | `rhinestoneApiKey` | `string` | — | API key for account setup | ### Backend | Prop | Type | Default | Description | | ------------ | -------- | ------------------------- | ------------------------------------------- | | `backendUrl` | `string` | Rhinestone production URL | URL of your deposit-widget-backend instance | ### Display | Prop | Type | Default | Description | | --------------------- | ---------------------- | ------- | ------------------------------------------------------------------- | | `inline` | `boolean` | `false` | Render without modal overlay | | `closeOnOverlayClick` | `boolean` | `true` | Close modal on backdrop click | | `className` | `string` | — | CSS class for the modal container | | `theme` | `DepositModalTheme` | — | [Theme configuration](/deposits/widget/customization#theme) | | `branding` | `DepositModalBranding` | — | [Branding configuration](/deposits/widget/customization#branding) | | `uiConfig` | `DepositModalUIConfig` | — | [UI configuration](/deposits/widget/customization#ui-configuration) | | `debug` | `boolean` | `false` | Enable debug logging | ### Callbacks | Prop | Type | Description | | --------------------- | -------------------------------------------- | -------------------------------- | | `onReady` | `() => void` | Modal initialized | | `onConnected` | `(data: ConnectedEventData) => void` | Smart account created | | `onWithdrawSubmitted` | `(data: WithdrawSubmittedEventData) => void` | Withdrawal transaction submitted | | `onWithdrawComplete` | `(data: WithdrawCompleteEventData) => void` | Tokens arrived on target chain | | `onWithdrawFailed` | `(data: WithdrawFailedEventData) => void` | Withdrawal failed | | `onError` | `(data: ErrorEventData) => void` | Error at any stage | | `onEvent` | `(event: WithdrawEvent) => void` | Analytics event | # Intents and ERC-4337 Source: https://docs.rhinestone.dev/home/concepts/intents-and-erc4337 How Rhinestone intents compare to ERC-4337 userops, and what makes intent-based execution different. ## What are intents? ERC-4337 introduced userops: pseudo-transactions handled by bundlers that relay smart account transactions onchain. Bundlers front the gas and submit through the EntryPoint contract, which orchestrates validation and execution via the smart account. **Intents extend this model to crosschain transactions.** An intent is a signed declaration of what the user wants to happen, which chains are involved, and which tokens they're spending. A solver fulfils it, fronting gas and liquidity across all chains involved. The same model as a bundler and paymaster, but capable of spanning multiple chains in a single signed operation. ## Intents vs ERC-4337 The key difference is the relayer layer. ERC-4337 routes through a single bundler/paymaster provider. Intents route through a competitive solver market where each solver manages token inventory across chains, aggregates DEX liquidity, and optimizes route and price independently. This is what enables crosschain transactions, built-in swaps, multi-input intents, and fee sponsorship that covers bridging and swap costs — not just gas. ## ERC-4337 Userop Flow ## Rhinestone Intent Flow Rhinestone uses intents to relay smart account transactions. The Relayer Market acts as bundler and paymaster. The Intent Executor, an ERC-7579 module installed on the smart account, handles intent signature validation and execution — without privileged execution rights on the account. Signature validation and execution are coupled onchain. In the case of ERC-7579 smart accounts, the Intent Executor uses [the executeFromExecutor function](https://eips.ethereum.org/EIPS/eip-7579#execution-behavior). *Intents and ERC-4337 userops are not mutually exclusive. A smart account can use both, depending on the flow.* ## Feature comparison | Feature | ERC-4337 (userops) | Rhinestone Intents | | :----------------------------------- | :----------------- | :----------------- | | Gas sponsorship | Yes | Yes | | Fee sponsorship (bridge + swap fees) | No | Yes | | Counterfactual addresses | Yes | Yes | | Built-in swaps | No | Yes | | Built-in bridges | No | Yes | | EOA support | No | Yes | ## When to use which | Scenario | Use | | ------------------------------------------------ | ---------------- | | Same-chain transaction, smart account only | ERC-4337 userops | | Crosschain transaction (any chain to any chain) | Warp intent | | Built-in swap needed (bridge + swap in one step) | Warp intent | | Sponsoring gas only, no bridging | Either | | EOA user (no smart account) | Warp intent | | Multi-input (aggregate funds across chains) | Warp intent | | Need ERC-4337 bundler compatibility | ERC-4337 userops | | AI agent executing across chains | Warp intent | ## Trust assumptions ERC-4337 userops today are almost universally sent to a single private provider despite the open mempool standard. Intent systems face the same pattern — intents are propagated to a provider, and if that provider is down, the intent isn't processed. Rhinestone's Orchestrator is trustless from the user's perspective. It can only interact with a smart account via the onchain execution paths defined by the Intent Router and Intent Executor. It poses no liveness or censorship risk. An onchain escape hatch can be activated without any dependency on Rhinestone. # Session keys Source: https://docs.rhinestone.dev/home/concepts/session-keys How session keys work in Rhinestone and what they enable. Session keys are restricted, short-lived keys that a user delegates to a third party. The delegate can sign transactions on the user's behalf, but only within the bounds the user defined. The user's main account key is never shared. Rhinestone's [Smart Sessions](../../smart-wallet/smart-sessions/overview) module implements session keys as an ERC-7579 validator. It is multichain-compatible and works with all supported smart account implementations and smart EOAs. ## How they work When a user creates a session, they define: * **Owners**: who holds the session key (your app, an agent, a smart contract) * **Actions**: which contracts and functions the key is allowed to call * **Policies**: constraints on those actions (spending limits, time windows, usage limits) The user signs once to approve the session. After that, the session key holder can execute permitted transactions without prompting the user again — until the session expires or its limits are reached. ## What they enable **1-click UX**: users pre-approve a set of actions, then your app executes them instantly with no wallet prompts. Common in trading apps, games, and subscription flows. **Automated transactions**: delegate to a bot, agent, or smart contract to execute on the user's behalf within predefined limits. The user retains full control and can revoke at any time. **Crosschain automation**: Smart Sessions is multichain-aware. A single session approval can authorize actions across multiple chains, consistent with Warp's single-signature intent model. ## Trust model Session keys do not grant unrestricted access to the account. They are constrained by: * The specific contracts and function selectors allowed in the session * Any policies applied (spending limits, timeframe, usage count) * The session expiry A compromised or malicious session key can only act within these bounds. The user's main key always retains the ability to revoke the session. ## Next steps Full API reference: owners, actions, policies, and multi-session signatures. Build a working session key integration step by step. # Smart accounts Source: https://docs.rhinestone.dev/home/concepts/smart-accounts How modular smart accounts work and what they enable. A smart account is a smart contract that holds assets and contains the logic for authenticating signatures and executing transactions. Unlike an EOA, its behaviour is programmable — you can extend it with modules to add new capabilities without changing the underlying account contract. Rhinestone smart accounts follow the [ERC-7579](https://erc7579.com) standard for modular smart accounts. ## Modules Modules are self-contained smart contracts that extend a smart account's feature set. There are four types: * **Validators**: control how transactions are authenticated. Examples: ECDSA key, passkey, multisig, session key. * **Executors**: enable executions on the account with custom logic. Examples: automated trading, scheduled transfers, stop-loss triggers. * **Hooks**: run before or after execution to enforce conditions. Examples: spending limits, transaction guards. * **Fallbacks**: extend the account interface to add new functionality or maintain compatibility with future standards. ## Signers Every smart account must have at least one validator configured. The entity controlling that validator owns the account. Rhinestone supports: * Passkeys (WebAuthn / device biometrics) * Embedded wallets (Privy, Dynamic, Turnkey, Magic, and others) * External wallets (MetaMask, Rabby, any WalletConnect-compatible wallet) * ECDSA keys (server-side or agent wallets) ## Resources The minimal standard for modular smart accounts: module types, interfaces, and interoperability. Tools, documentation, and example code for the ERC-7579 smart account ecosystem. Building modular accounts with ERC-7579: validators, executors, hooks, fallbacks, and social recovery. # How Warp works Source: https://docs.rhinestone.dev/home/introduction/rhinestone-intents A technical overview of Warp, Rhinestone's intent routing and execution engine. **Warp** is Rhinestone's intent routing and execution engine. It aggregates crosschain settlement layers through a unified solver market and executes intents with sub-2-second confirmation times.