POST /api/embed/siws/nonce

Starts a wallet submission. The server mints a short-lived nonce, binds it to {trackingPageId, walletAddress, API key}, and returns the exact message the visitor's wallet must sign.

Used together with POST /submissions. Full end-to-end explanation in SIWS signing.

Request

POST /api/embed/siws/nonce HTTP/1.1
Host: walletsleuth.app
Authorization: Bearer ws_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Origin: https://example.com
Content-Type: application/json

{
  "trackingPageId": "abc123",
  "walletAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
}

Headers

HeaderRequiredNotes
AuthorizationyesBearer <api_key>.
Originyes for public keysMust be on the key's allowlist.
Content-Typeyesapplication/json.

Body

FieldTypeRequiredNotes
trackingPageIdstringyesTracking page ID. Must match the key.
walletAddressstringyesBase58 Solana address. Validated via @solana/web3.js PublicKey.

Success response

200 OK:

{
  "nonceId": "6f7a1e4a-5b3f-4b3e-8a2c-e2d8c1b4f7a1",
  "message": "Wallet Sleuth Verification\n… lines …\nThis signature does not authorize any transaction."
}
FieldTypeNotes
nonceIdstringPass this back in the submission body.
messagestringExactly the bytes the wallet must sign (UTF-8). Treat as opaque; do not reconstruct client-side.

The nonce is short-lived and single-use: the first successful call to POST /submissions consumes it. If you wait too long, request a fresh nonce.

Error responses

StatuserrorMeaning
400invalid_jsonRequest body wasn't valid JSON.
400missing_fieldstrackingPageId or walletAddress missing / wrong type.
400invalid_wallet_addresswalletAddress isn't a valid base58 Solana address.
401missing_keyNo Authorization: Bearer ….
401invalid_key / revoked_keySee Authentication.
401missing_originPublic key with no Origin/Referer.
403origin_not_allowedOrigin not on the key's allowlist.
429rate_limit_key / rate_limit_ipGlobal rate limits. Retry-After included.
429rate_limit_sessionSession nonce limit hit. Retry-After included.

Session nonce limit

Sessions (set by the iframe route via the embed_session cookie) have a cap on nonce issuance. This prevents a bad actor from spamming nonce issuance.

If the caller has no session cookie (e.g. a custom UI that never loaded the iframe), the session-specific limit is skipped — but per-key and per-IP limits still apply.

What to do with the response

  1. Take message and convert it to UTF-8 bytes.
  2. Ask the visitor's wallet to sign those bytes.
  3. Base64-encode the 64-byte Ed25519 signature.
  4. POST it back to POST /submissions with the nonceId.

Example (fetch)

const nonceRes = await fetch(
  'https://walletsleuth.app/api/embed/siws/nonce',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${publicKey}`,
    },
    credentials: 'include',
    body: JSON.stringify({ trackingPageId, walletAddress }),
  },
);
const { nonceId, message } = await nonceRes.json();

// Sign via wallet adapter
const signatureBytes = await wallet.signMessage(new TextEncoder().encode(message));
const signature = Buffer.from(signatureBytes).toString('base64');

Preflight (OPTIONS)

Returns 204 with:

Access-Control-Allow-Origin: <request origin>
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 600
Vary: Origin

Related