Skip to main content

The embed

public/sdk.js mounts an iframe pointing at the hosted checkout. The merchant pins their settlement anchor in the init call on their own site:
The anchor is the root of the client-side verification chain. It lives on the merchant’s own site, which is precisely why a compromise of the platform’s database cannot reach it. See DB-display-only trust.

The SRI pin

Any edit to public/sdk.js MUST regenerate SDK_SRI in src/lib/sdk/embedSnippet.ts. A stale pin makes browsers refuse to execute the SDK entirely — every embed on every merchant site stops working.
The pin is not optional decoration. SRI is what makes the served-code threat mitigated rather than unaddressed — without it, a compromise of the served bundle would defeat the entire client-side verification argument.

Three failures that made the embed DOA

Found in the 2026-07-28 audit. All fixed, all worth understanding because each was invisible in normal testing.
The iframe was sandboxed without allow-same-origin, so the app crashed at boot the moment it touched localStorage. The embed had never worked.
The apex 307-redirects to www, and the redirect broke the origin check. The snippet must use the canonical www host.
CHECKOUT_PATH was hardcoded to /checkout — the legacy atomic widget — rather than /pool-checkout. That silently dropped the pinned anchor, which meant the verification the anchor exists for never ran.
The third is the instructive one: the embed appeared to work, payments went through, and the trust guarantee was quietly absent.

Bundle budget

App.tsx once statically imported the wallet shell, putting wallet-vendor (3.4 MB) and viem-vendor into the entry chunk. Every route, including the landing page, shipped 5.0 MB with a 4,908 ms first contentful paint.
Now route-aware and lazy via config/walletRoutes.ts and contexts/WalletBootContext.tsx: 923 KB, 188 ms. scripts/check-bundle-budget.mjs fails CI if wallet-vendor becomes statically reachable from the entry again.
Do not “fix” a build error by re-adding a static import of @/config/appkit. That is the exact regression the budget check exists to catch.

Content Security Policy

CSP is set in two places — the vercel.json HTTP header and the <meta http-equiv> tag in index.html. When both are present the more restrictive applies. Patch both or it silently breaks.
Origins that have broken production before and will again: Third-party FX APIs — frankfurter.app, exchangerate.host, open.er-api.com — are deliberately not allowlisted. EUR/USD comes from an on-chain Uniswap V3 pool, never a third-party API.

The SPA rewrite trap

The SPA fallback rewrite must exclude /api/:/((?!api/|assets|sdk\.js|favicon\.ico|robots\.txt|placeholder\.svg).*)Without that exclusion, Vercel rewrites /api/quote-uniswap to index.html and the request 404s in a way that looks like an application bug.

Checkout states

The widget has explicit states for every failure mode rather than a generic error: Two resilience behaviours worth knowing:
  • Quote auto-refresh. A silent re-quote 30 seconds before expiry plus a 3-second “rate updated” pill, so a customer is never surprised by a number that moved while they were about to pay.
  • Closed-tab recovery. The active payment intent is stamped into local storage on broadcast. On the next mount the widget calls payment-status: confirmed goes straight to success, failed clears the key, still-confirming leaves it for the next mount.