Skip to main content

The button does nothing

Check the console first — the loader logs rather than throws on the declarative path. Nothing in the console at all? The element was probably never bound. bindInvestButtons runs on DOMContentLoaded and binds each element once — if your button is rendered afterwards, call window.Cesto.bindInvestButtons({ apiKey }) again. It’s idempotent and returns the number of elements newly bound.

The popup was blocked

invest() opens a window synchronously on the popup path. Browsers only allow that inside a user gesture, so this breaks it:
This applies even in dialog mode. The dialog can fall back to a popup, and once the fallback is latched for the tab, the next invest() opens a window directly. Never put an await or a timer between the click and the call.
If it’s blocked anyway, the loader falls back to a full-page navigation to the invest URL — the flow completes, but the SDK can’t observe the outcome and the visitor isn’t returned to your page. See Modes.

Callbacks never fire (popup mode)

Check for Cross-Origin-Opener-Policy: same-origin on the embedding page. It severs window.opener, so the invest completes but nothing reports back. Serve the page with same-origin-allow-popups (or no COOP header), or switch to dialog mode, which is unaffected. Details: COOP and popup mode.

onVerified never fires

The origin isn’t on your key’s allowlist. The flow still works — it’s just unbranded and unattributed, which is usually what someone means when they say “it works but it looks wrong”. Two things to check:
  1. The exact origin, including scheme, host, and port. https://www.example.com and https://example.com are different origins.
  2. Preview and staging deployments — a Vercel preview URL changes per deployment and won’t be on the list.
Ask the Cesto team to add the origins you need. See API keys.

The dialog never appears

The overlay swaps to a “Continue in new window” panel after 20 seconds if the iframe doesn’t become ready. Common causes: a privacy extension blocking third-party frames, a network failure, or a browser that won’t frame the app. The failure is remembered for the rest of the tab session (sessionStorage key cesto-invest:force-popup), so later calls go straight to popup mode. Clear the tab’s session storage — or open a new tab — while debugging, or you’ll keep testing the popup path by accident.

The button renders unstyled (React)

A strict style-src without 'unsafe-inline' blocks the runtime stylesheet injection. Import the stylesheet and mark the document instead:
See React → Strict CSP.

The theme is ignored

Values are validated server-side, field by field, and anything invalid silently falls back to the Cesto default. Check the format before the wiring:
  • Colors must be #rrggbb. Not #rgb, not rgb(), not oklch(), not rebeccapurple.
  • radius must be a CSS length, and is capped at 32px / 2rem.
  • scheme must be exactly dark, light, or auto.
Reading tokens off your own stylesheet is a common trap — a design token defined in oklch resolves to oklch(...) and gets dropped. See Theming.

The user is asked to sign in again

Expected in dialog mode. The iframe runs with partitioned storage, so a session on app.cesto.co is invisible inside the frame. It is a browser guarantee, not something the SDK can opt out of. Popup mode shares the app session.

A wallet prompt appears over my site

Also expected, and also dialog mode. Wallet extensions don’t inject providers into cross-origin iframes, so the SDK relays detect / connect / sign requests from the frame on your top-level page. Your page can only add signatures — it can never alter what is being signed. See the wallet relay.

onClose fired — did the invest fail?

No. onClose means the surface was dismissed. It also fires after a success. Only onError means the invest failed, and only onPositionClosed means a position was closed. See Events.

Server-side rendering errors

@cesto/react is client-only and ships a 'use client' banner, so importing it from a server component is fine — but the callbacks you pass are functions and cannot cross the server boundary. Move any component that passes callbacks into a 'use client' file. See Next.js App Router. In a non-browser environment the React bindings fire onError with code: 'config_error' rather than throwing.

Testing against a local app

1

Run the Cesto app locally

On http://localhost:3000.
2

Point the loader at it

new Cesto({ apiKey, baseUrl: 'http://localhost:3000' }), or data-cesto-base-url="http://localhost:3000" on the button.
3

Serve your test page from an allowlisted origin

For example http://localhost:8787. The origin has to be on the key’s allowlist or verification won’t fire.
4

Watch the events

onReady (frame or popup booted), then onVerified once the backend validates the key and origin, then the invest lifecycle events.
Two ports on localhost are the same site to the browser, so the dialog iframe is never treated as third-party locally. Storage partitioning and the framing rules that come with it will not reproduce — test those on a real cross-site origin before you conclude they work.

Still stuck?

Reach out on the Cesto community channel with your publishable key’s partner name, the origin you’re embedding from, the mode, and anything the console logged.