backendUrl. There is no default — point it at a proxy you run, on your own key.
Two ways to get one:
- Deploy Rhinestone’s. Open source and configurable — see deploy the Rhinestone proxy.
- Write your own. A route table and a header — see minimal proxy.
Deploy the Rhinestone proxy
rhinestonewtf/deposit-widget-proxy is the proxy Rhinestone runs, packaged so you can deploy it as-is. It covers every route in the table below, and adds regional payment methods — which a hand-written proxy can’t do, since resolving the user’s country needs the edge that actually sees them.
It needs one variable — your API key:
backendUrl at it and check GET /health. Everything else is optional and documented in the repository’s README:
Minimal proxy
A proxy is an explicit route table plus a header. The allowlist is the security boundary — see the warning below.Recovery can be forwarded; refunds cannot
These two look alike and differ in exactly one way: where the authorization comes from.POST /deposits/recover carries a signature from the deposit’s recipient, covering
which deposit and which destination. The service verifies it before moving anything,
so your API key on its own achieves nothing here — which is what makes it safe to
forward like any other route. See
claim modal.
POST /deposits/refund carries no such proof. Every route in that loop passes the
browser’s body through with your API key attached, so forwarding this one would let
anyone return any of your recoverable deposits to an address they chose. A proxy
authenticates nobody, so it cannot be the thing that decides.
Most apps need only the recover route. If some of your recipients genuinely cannot
sign, authorize a refund in your own backend with
createRefundHandler, which
checks the deposit belongs to the caller before spending the key, and call the
processor directly.
Required routes
Missing a route doesn’t degrade the flow — the request 404s and that part of the modal stops working. Two routes are load-bearing rather than partial: without/chains the modal has no chain set at all, and without /register-managed there is no deposit account to send to. Everything else costs you the feature in its row.
The analytics token route
Analytics events themselves do not pass through your proxy — the modal posts them straight to Rhinestone, which is why the ingest origin belongs in your app’s content security policy rather than in your proxy. The token is the proxy’s only analytics involvement. Serve it like every other route, on the server-held key, and mark the responseCache-Control: no-store — the token is per-session and short-lived, so a shared
cache must not hand one session’s token to another. It needs no new browser
request header, no CORS allowance and no configuration, so a proxy with a fixed
allowHeaders list needs no change.
A proxy without the route leaves sessions unattributed. Collection continues and
deposits are untouched; the modal reports
token_unavailable once and
stops asking.
CORS, origins and the version header
Mobile integrations call from our origin, not yours
On the web the modal runs inside your page, so its requests carry your own domain as theOrigin. A mobile integration does not: the native wrapper loads the modal from a page we host, and every backend call it makes comes from
https://dev.deposit.rhinestone.dev if you are pointed at a dev integration). Scheme and host only — a CORS origin never carries a port or a path.
If your proxy sets an explicit origin: [...] allow-list, add it there:
cors() reflects whatever origin asks, so a proxy left on the default — including Rhinestone’s — needs no change.
The version header
All three modals sendx-deposit-modal-version on every request. Browsers reject a request carrying a header the server didn’t allow on the preflight, so an explicit allow-list must include it:
cors() in Hono is fine — with no allowHeaders it reflects whatever the preflight asks for.
Forwarding it upstream is optional, but it lets a support request be matched to the exact build you’re running. To read the value in your own app, for a bug report:
Regional payment methods
Fiat on-ramp methods vary by country, and your proxy is the only component that can see the end user: the processor sits behind it and only ever observes your proxy’s address. SoGET /onramp/swapped/payment-methods returns the generic method set unless your proxy names the user’s region.
You do not need a GeoIP database — the processor owns the lookup. The proxy only names what it observed, which takes one of two variables:
TRUSTED_COUNTRY_HEADER— you’re behind a CDN that already resolves country, so forward its header (cf-ipcountry,x-vercel-ip-country,cloudfront-viewer-country).TRUSTED_PROXY_HOPS— nothing resolves it for you, so relay the client IP and let the processor resolve it.
TRUSTED_PROXY_CIDRS, an allowlist of the peers permitted to set forwarding headers. Without it any browser could send x-forwarded-for or cf-ipcountry and choose its own region, so Rhinestone’s proxy refuses to start when you set one without the other. See its README for the details, including why hops are counted from the right.
Every path here fails closed. A wrong setting costs you localization, not correctness: you get the generic method set rather than a region that isn’t the user’s. A hand-written proxy that relays nothing behaves exactly as it does today.
Webhooks
The widget’s lifecycle callbacks fire only while the modal is open, so a user who closes it mid-bridge leaves your app unaware the deposit completed. Anything that must happen regardless — crediting a balance, sending a receipt — belongs on a webhook handler. Configure it once withPOST /setup.