Auth (SIWE)
Nonce, wallet signature, JWT Bearer token.
Protected API routes use a JWT from Sign-In with Ethereum. Get a nonce message, sign it with the wallet, then exchange signature + message for a token.
Steps
POST /auth/noncewith{ "wallet": "0x…" }.- Sign the returned
messagewith the wallet. POST /auth/loginwith wallet, signature, and message.- Send
Authorization: Bearer <token>on protected routes.
bashauth-curl.sh
WALLET=0xYourWallet
# 1) Nonce
curl -sS -X POST "${API_URL}/auth/nonce" \
-H "content-type: application/json" \
-d "{\"wallet\":\"$WALLET\"}"
# 2) Sign the returned message in your wallet, then:
curl -sS -X POST "${API_URL}/auth/login" \
-H "content-type: application/json" \
-d "{\"wallet\":\"$WALLET\",\"signature\":\"0x…\",\"message\":\"<exact message>\"}"
# 3) Authenticated call
curl -sS "${API_URL}/auth/me" -H "authorization: Bearer $JWT"tssiweLogin.ts
import { createWalletClient, custom } from "viem";
const API = process.env.API_URL!; // e.g. https://api.example.com
async function login(address: `0x${string}`) {
const nonceRes = await fetch(`${API}/auth/nonce`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ wallet: address }),
}).then((r) => r.json());
if (!nonceRes.success) throw new Error(nonceRes.error);
const { message } = nonceRes.data as { nonce: string; message: string };
const wallet = createWalletClient({ transport: custom(window.ethereum) });
const signature = await wallet.signMessage({ account: address, message });
const loginRes = await fetch(`${API}/auth/login`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ wallet: address, signature, message }),
}).then((r) => r.json());
if (!loginRes.success) throw new Error(loginRes.error);
return loginRes.data.token as string; // Authorization: Bearer …
}Troubleshooting
- Message must match exactly (including domain configured as SIWE_DOMAIN).
- Wallet in login must match the signer.
- 401 on protected routes: refresh JWT via login again.