Authentication
Authentication (SIWE)
Authenticate using Sign-In with Ethereum (SIWE). This exchanges a wallet signature for a secure session cookie used in write endpoints (images, metadata, projects, comments).
API key or session
- The API key wins. When a request carries
X-API-Key, the key's wallet is the caller, even if a session cookie is sent too. The key's rate limit applies (60 a minute per key). - An unknown or revoked key answers
401with codeinvalid_key. It never falls back to the session. - Without a key, the session cookie is used (30 a minute per IP).
- Session-only routes always need the session and ignore the key: key management (
/api/v1/auth/keys), sign-out (DELETE /api/auth/me), onboarding and X sign-in.
Endpoints
GET
/api/auth/nonce?address={wallet_address}
Generates a one-time nonce for the given wallet address. Must be called before signing. 30 a minute per IP.
Response
{ "nonce": "0192f3a1b2c4…" }- A nonce is valid for 5 minutes and belongs to the wallet it was asked for.
- Asking for a nonce never cancels another one, so any number can be open at once (two tabs, a script and the dapp).
- Each nonce signs in once. A sign-in also retires that wallet's nonces issued before it, so sign with the newest one you asked for.
- Treat the nonce as an opaque string: copy it into the message as you got it.
POST
/api/auth/verify
Verifies the signed SIWE message. Returns a Set-Cookie header with your session.
Request Body
Response
{
"message": "the prepared SIWE message string",
"signature": "0x..."
}Message rules
domainmust be this site's host andurimust be this site's origin (for examplehttps://launchonbasis.com). Either one wrong answers422with codedomain_mismatch.chainIdmust be the chain this deployment runs on. Another chain answers422with codewrong_chain: switch the wallet and sign again.noncemust be a fresh nonce from the endpoint above for the same wallet. An expired, used or foreign nonce answers422with codeinvalid_nonce.- The JSON body is capped at 16 KB (
413payload_too_large).
Response
Response
{ "ok": true, "address": "0x..." }200 OK400 Malformed body or message413 Body too large422 Invalid nonce, signature, domain or chain429 Rate limited
Node.js Example
Example Code
import { ethers } from "ethers";
import { SiweMessage } from "siwe";
const BASE_URL = "https://launchonbasis.com";
async function authenticate() {
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
const address = await wallet.getAddress();
// 1. Get nonce
const nonceRes = await fetch(`${BASE_URL}/api/auth/nonce?address=${address}`);
const { nonce } = await nonceRes.json();
// 2. Create & sign SIWE message
const message = new SiweMessage({
domain: "launchonbasis.com",
address,
statement: "Sign in to Basis API.",
uri: BASE_URL,
version: "1",
chainId: 4663,
nonce,
});
const preparedMessage = message.prepareMessage();
const signature = await wallet.signMessage(preparedMessage);
// 3. Verify — returns session cookie
const verifyRes = await fetch(`${BASE_URL}/api/auth/verify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: preparedMessage, signature }),
});
// Extract the session cookie for subsequent requests
const sessionCookie = verifyRes.headers.get("set-cookie");
return sessionCookie;
}Python Example
Example Code
import requests
from eth_account import Account
from eth_account.messages import encode_defunct
from datetime import datetime
import os
BASE_URL = "https://launchonbasis.com"
private_key = os.environ["PRIVATE_KEY"]
account = Account.from_key(private_key)
address = account.address
# 1. Get nonce
nonce = requests.get(f"{BASE_URL}/api/auth/nonce?address={address}").json()["nonce"]
# 2. Construct & sign SIWE message
issued_at = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ")
message = (
f"launchonbasis.com wants you to sign in with your Ethereum account:\n"
f"{address}\n\nSign in to Basis API.\n\n"
f"URI: {BASE_URL}\nVersion: 1\nChain ID: 4663\n"
f"Nonce: {nonce}\nIssued At: {issued_at}"
)
signature = Account.sign_message(
encode_defunct(text=message), private_key=private_key
).signature.hex()
# 3. Verify: the session object stores the cookie automatically
session = requests.Session()
session.post(f"{BASE_URL}/api/auth/verify", json={"message": message, "signature": signature})
# Use 'session' for all subsequent requests