For the complete documentation index, see llms.txt. This page is also available as Markdown.

Sign in with Ethereum (SIWE)

Kumbaya's Client API at clients.kumbaya.xyz supports EIP-4361 (Sign-In With Ethereum) as a first-class authentication path. This is the right path for agents, bots, and any integration with its own private key - no Privy account, no social login.

A wallet that signs in via SIWE for the first time is automatically registered as a user with a synthetic privyDid: "wallet:<address>". From there it has the same JWT-authenticated access as any Privy-backed account.

Endpoints

Endpoint
Method
Auth
Purpose

/v1/session/wallet/nonce

GET

None

Issue a one-time nonce for an address (5-minute TTL)

/v1/session/wallet/verify

POST

None

Verify a signed SIWE message, issue a JWT

Flow

agent                                       client-api
  │                                              │
  │  GET /v1/session/wallet/nonce?address=0x..   │
  ├─────────────────────────────────────────────▶│
  │                                              │   stores nonce in Redis
  │  { "nonce": "abc..." }                       │   under key siwe:nonce:0x...
  │◀─────────────────────────────────────────────┤
  │                                              │
  │  build SIWE message with the nonce           │
  │  personal_sign(message)                      │
  │                                              │
  │  POST /v1/session/wallet/verify              │
  │       { message, signature }                 │
  ├─────────────────────────────────────────────▶│
  │                                              │   parses message → recovers signer
  │                                              │   compares to nonce-bound address
  │                                              │   consumes nonce (one-shot)
  │                                              │   creates user if first-time
  │  { token, expiresAt, user }                  │   issues JWT (also as httpOnly cookie)
  │◀─────────────────────────────────────────────┤

Required SIWE message format

The backend uses a minimal parser. It only requires:

  • The signer's address (anywhere in the message, matched as the first 0x[0-9a-fA-F]{40})

  • A line of the form Nonce: <nonce>

It accepts the standard EIP-4361 layout, e.g.:

Sign with personal_sign (the EIP-191 prefixed-message variant). viem's signMessage, ethers' signer.signMessage, and most wallet libraries default to this.

End-to-end example (viem)

Using the JWT

Pass the token on subsequent requests via either:

  • Bearer header: Authorization: Bearer <jwt>

  • httpOnly cookie: the verify response also sets a session cookie automatically (use this if your agent shares a cookie jar)

Refresh

JWTs are short-lived. Call POST /v1/session/refresh (with the existing JWT) to rotate, or simply re-run the SIWE flow when your token expires.

For long-running agents, the simplest pattern is: request a fresh JWT on startup, refresh on a timer, re-do SIWE on any refresh failure.

Errors

Status
Body
Cause

400

{ "error": "Invalid SIWE message format" }

Couldn't parse address or nonce from the message

401

{ "error": "Invalid or expired nonce" }

Nonce doesn't match what was issued, or expired (5 min)

401

{ "error": "Signature verification failed" }

Recovered signer ≠ message address

503

{ "error": "Service unavailable" }

Backend can't reach Redis (nonce store)

Notes for production

  • Nonce TTL is 5 minutes. Don't pre-fetch nonces hours ahead.

  • Nonces are single-use. Reusing a nonce after a successful verify will fail.

  • The address-in-message must match the recovered signer. No "claim someone else's account" loophole.

  • Wallet-only users get a synthetic privyDid of wallet:<address>. If a Privy user later imports the same wallet, the records merge implicitly - but for agent-only flows, this is usually moot.

  • You can sign in from any chain. The Chain ID field in the SIWE message is informational; the parser doesn't enforce it. (That said, for clarity, set it to 4326 mainnet or 6343 testnet.)

Last updated