“Sign in with VIZ” is passwordless auth backed by the blockchain: a user proves they control an account by signing a challenge with their key, and your app gets a session token in return. No passwords to store, no email flow — the same key that signs transactions signs the login.
The flow is a signature challenge: fetch a one-time nonce, sign it, exchange the proof for a bearer token. viz.cx's own watchlist and notifications run on exactly this.
Step 1 — Build a signer
npm install @viz-cx/api @viz-cx/coreA Signer is an account name plus a function that signs bytes with its regular key.
import { createApiClient } from '@viz-cx/api'
import { keys, wif } from '@viz-cx/core'
// A Signer proves control of an account by signing challenges with its
// regular key. In a real dApp this comes from the user's wallet — here we
// load the WIF from the environment for a server-side or CLI login.
const signer = {
account: 'alice',
sign: (bytes: Uint8Array) => keys.sign(bytes, wif(process.env.VIZ_REGULAR_KEY!)),
}
const api = createApiClient({ auth: signer })Step 2 — Exchange a signature for a token
authedFetch handles the nonce round-trip and header signing, so hitting POST /session returns a 30-day bearer token.
// authedFetch runs the whole challenge for you:
// 1. POST /auth/nonce -> a single-use nonce (valid 5 min)
// 2. sign the nonce with the key
// 3. resend with X-Auth-Account / X-Auth-Nonce / X-Auth-Signature
// Exchange that proof for a 30-day bearer token.
const res = await api.authedFetch('/session', { method: 'POST' })
if (!res.ok) throw new Error('login failed')
const { token } = await res.json()
console.log('Session token:', token) // store it (cookie / secure storage)Step 3 — Call session-gated endpoints
Send the token as Authorization: Bearer <token>. No signing per request — the token is the session.
// Use the bearer token on session-gated endpoints — no more signing.
const base = 'https://api.viz.cx'
const auth = { Authorization: `Bearer ${token}` }
// e.g. add an account to the signed-in user's watchlist
await fetch(`${base}/watchlist`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify({ account: 'bob' }),
})
const count = await fetch(`${base}/notifications/count`, { headers: auth }).then(r => r.json())
console.log('unread:', count)Step 4 — Log out
DELETE /sessionrevokes the token immediately. It's idempotent, so double-logout is fine.
// Log out — revoke the token server-side. Idempotent.
await fetch('https://api.viz.cx/session', {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
})Nonces are single-use and expire in 5 minutes; tokens last 30 days or until revoked. In the browser, get the signature from the user's wallet rather than handling their raw key.