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 401 with code invalid_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

  • domain must be this site's host and uri must be this site's origin (for example https://launchonbasis.com). Either one wrong answers 422 with code domain_mismatch.
  • chainId must be the chain this deployment runs on. Another chain answers 422 with code wrong_chain: switch the wallet and sign again.
  • nonce must be a fresh nonce from the endpoint above for the same wallet. An expired, used or foreign nonce answers 422 with code invalid_nonce.
  • The JSON body is capped at 16 KB (413 payload_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