SIWS signing

When a visitor submits a wallet through the widget, they must sign a Sign-In-With-Solana (SIWS) message proving they own the key. This page explains the flow, the message format, and why the widget has both an in-frame signing path and a popup fallback.

The flow

  1. The widget calls POST /api/embed/siws/nonce with the tracking page ID and the wallet address the visitor intends to prove. See POST /siws/nonce.
  2. The server returns a nonceId and the full message to sign.
  3. The visitor's wallet signs message bytes (UTF-8). The widget encodes the resulting signature as base64.
  4. The widget calls POST /api/embed/submissions with the signature, nonce ID, nickname, and wallet address. See POST /submissions.
  5. The server verifies the signature against the rebuilt message, runs policy checks, and writes the submission.

The message format

The server builds the message deterministically so callers never need to construct it themselves. The server returns the full message; treat it as opaque and sign it byte-for-byte.

The last line of the message is a human-readable disclaimer shown inside wallet UIs: "This signature does not authorize any transaction."

The server rebuilds the exact message when verifying the signature; any discrepancy (different line endings, altered whitespace) fails with 401 invalid_signature. Don't reconstruct the message on the client.

Why there's a popup fallback

When the widget is inside an iframe on a third-party site, some wallet extensions refuse to pop a signing prompt. Phantom in particular does not reliably expose signMessage to sandboxed iframes. To work around this, the widget offers two signing paths:

In-frame signing (default)

The visitor clicks "Connect wallet" inside the iframe (via @solana/wallet-adapter). If the adapter surfaces a working signMessage, the widget signs directly and submits.

Popup signing (fallback)

The visitor clicks "Can't sign in-frame? Use popup" (or enters a wallet address manually). The widget opens a signing page at the Wallet Sleuth origin in a new window.

The popup page runs at the Wallet Sleuth origin, which is a regular top-level browsing context — wallet extensions treat it like any other dApp and sign freely. After the visitor signs in the popup, the signature is posted back to the iframe and the submission completes.

When does the popup path trigger? Automatically when the visitor chooses "Enter address manually" (no wallet-adapter connection available), and manually via the "Use popup" link under the form. If you're building your own UI off the API, implement either path yourself — the server doesn't care which was used.

What visitors actually sign

Nothing. The final line of the message makes this explicit: "This signature does not authorize any transaction." It's a challenge-response proof of private-key ownership. No tokens move, no on-chain transactions are produced, and the signature is not replayable against any other system because it includes a Wallet Sleuth-scoped nonce.

Related