Skip to main content
The ClaimModal returns a failed or rejected EVM deposit to the user, on the chain it came from. The user pastes the deposit’s transaction hash, the modal looks it up, and the user signs to authorize where the funds go. There is no source chain to pick — the hash is looked up across every chain, so a deposit that landed on a chain your deposit flow doesn’t offer is still claimable.
On the ./claim subpath. Applies to deposits in failed or rejected status. See troubleshooting for the operator-side equivalent in the dashboard.

How authorization works

The deposit’s recipient — the in-app wallet the funds were headed to — signs an EIP-712 struct naming the deposit and the destination. The service verifies that signature against the same recipient before moving anything. That signature is the authorization, so this needs no backend of your own. The request goes from the browser through your proxy, which contributes only your project API key. That key cannot move funds through this route without a signature, so there is nothing for a page to borrow.
The modal never runs a connect step — it has no wallet UI and asks for no provider. It calls signRecovery and expects a signature back. For an embedded wallet that is headless, so the user sees a single confirmation at most.
The signer is the deposit’s recipient, never the deposit account. Deposit accounts are service-managed and hold no user key, so nothing can sign for them.
Your proxy must forward POST /deposits/recover alongside the GET /deposits used for the lookup. Both are in the reference proxy.

Signing

Return whatever the recipient address’s own verifier accepts. signRecovery receives the exact typed data to sign, so you never construct it yourself.
For a smart account, a raw owner signature is usually not enough. A Safe expects its own message wrapping, so go through your account SDK rather than signing with the owner key directly.
An undeployed account can still be verified, because an ERC-6492 wrapper carries the account’s factory and factory data. That makes the check work without the account existing on chain yet — which matters, since a deposit can fail before the user’s account is ever deployed.
ox is already a dependency of viem, so this adds nothing to your install.

What the user signs

Two fields, both meaningful to the person signing: which deposit, and where the money goes. The destination is inside the signature, which is the property worth understanding. The service cannot pay anywhere other than the address the user signed for, so a compromised page cannot redirect the funds — and your success screen can state where they went without trusting a response to echo it back. The signature is verified on the deposit’s target chain, where the recipient wallet lives, not the source chain the funds sit on. For a smart account with different owners per chain, its target-chain owners are the ones who can authorize.
A signature stays valid until the deposit is claimed — there is no expiry. Replay is already closed, because claiming moves the deposit out of failed/rejected and a second attempt is rejected. But do not persist a signature: treat it as single-use and discard it once the request returns.

Handling failures

The response carries a machine-readable code. Switch on that rather than the HTTP status — the code is the contract, and the correct advice differs between codes that share a status. The modal maps these to copy and to whether it offers a retry, so you get this for free. Handle them yourself only if you drive your own UI.
Never retry REFUND_RECONCILIATION_REQUIRED. The funds may already be moving, and the deposit needs an operator either way — contact support with the deposit.

When the user can’t sign

Some recipients have no key to sign with: a wallet the user has lost, or a recipient your app controls rather than the user. For those, @rhinestone/deposit-modal/server exports createRefundHandler, which authorizes on your say-so instead of a signature — you decide who the caller is, and it verifies the deposit belongs to them before spending your API key.
authorize returns the deposit recipient the caller owns, or null to reject with 401. Add refundDestination to pin where the money goes rather than letting the request choose. depositRecipient never decides the destination — the handler lists deposits settling to it and rejects a txHash that isn’t among them. It returns a (Request) => Promise<Response>, so it mounts in any fetch-based runtime: Next.js route handlers, Hono, Bun.serve, Cloudflare Workers.
Import from @rhinestone/deposit-modal/server only — it holds your API key and must never reach the browser. ClaimModal does not call this route; drive your own UI against it.
Never refund to an exchange deposit address. Exchanges don’t credit arbitrary incoming transfers, so the funds may be lost. This is why the modal never defaults the destination to the deposit’s sender.

Props reference

Required

signRecovery receives:

Prefills

Backend

Display

Callbacks

Lifecycle events

ClaimLifecycleEvent shares no variants with the deposit or withdraw unions, so don’t reuse a handler across them.