Getting Started

Quick start

Add copy-pasteable provider access and Nimiq Better Auth sign-in.

Use this path when the same browser runs inside Nimiq Pay and needs a Better Auth session for the current Nimiq key.

The goal is to turn an injected Nimiq wallet into a normal application session. The browser waits for the mini-app provider, the server issues a nonce, the wallet signs it, and Better Auth owns the resulting session cookie and bearer token.

Do not use this page for desktop-to-phone QR login. Use the cross-device package when approval happens on another device.

1. Install packages

Use PKG.new for installation:

2. Register the server plugin

server/auth.ts
import { betterAuth } from 'better-auth'
import { bearer } from 'better-auth/plugins'
import { nimiqAuth } from '@onmax/better-auth-nimiq'

export const auth = betterAuth({
  trustedOrigins: ['https://arcade.example'],
  plugins: [
    bearer({ requireSignature: true }),
    nimiqAuth({
      appName: 'Arcade Rewards',
      endpointPrefix: '/nimiq',
      tokenHeaderName: 'set-auth-token',
    }),
  ],
})

The plugin exposes two endpoints under the prefix:

EndpointPurpose
POST /nimiq/nonceIssues a short-lived Nimiq login challenge
POST /nimiq/verifyVerifies the signature and creates the Better Auth session

Expected behavior: /nimiq/verify sets the Better Auth session cookie and returns { ok: true, token }.

3. Register the client plugin

app/auth-client.ts
import { createAuthClient } from 'better-auth/client'
import { getStoredNimiqAuthToken, nimiqAuthClient } from '@onmax/better-auth-nimiq/client'

const TOKEN_KEY = 'nimiq_auth_token'

export const authClient = createAuthClient({
  plugins: [
    nimiqAuthClient({
      endpointPrefix: '/nimiq',
      tokenStorageKey: TOKEN_KEY,
    }),
  ],
  fetchOptions: {
    auth: {
      type: 'Bearer',
      token: () => getStoredNimiqAuthToken(TOKEN_KEY) || undefined,
    },
  },
})

The client helper persists the token in localStorage by default. The fetchOptions.auth block makes later authenticated requests send it as a bearer token.

4. Wait for the Nimiq Pay provider

app/provider.ts
import { initMiniAppProvider, isMiniAppProviderError } from '@onmax/nimiq-mini-app-kit'

export async function getSelectedNimiqAddress() {
  const provider = await initMiniAppProvider({ timeout: 10_000 })
  const accounts = await provider.listAccounts()

  if (isMiniAppProviderError(accounts))
    throw new Error(accounts.error.message)

  return accounts[0] ?? null
}

Expected behavior: inside Nimiq Pay, the helper resolves the injected provider and returns the selected Nimiq address. Outside the host, it rejects after the timeout.

5. Trigger sign-in

app/sign-in.ts
import { signInWithNimiq } from '@onmax/better-auth-nimiq/client'
import { authClient } from './auth-client'

const result = await signInWithNimiq(authClient.$fetch.bind(authClient), {
  appName: 'Arcade Rewards',
  endpointPrefix: '/nimiq',
  tokenStorageKey: 'nimiq_auth_token',
})

console.log(result.token)

Expected behavior: the wallet signs the challenge message, the server verifies the signature, and the browser stores the returned token.

6. Use the session

app/me.ts
import { authClient } from './auth-client'

const session = await authClient.getSession()

console.log(session.data?.user.id)

Expected behavior: authenticated Better Auth calls work through the session cookie and through the bearer token when the app runtime needs header-based auth.

Common failure states

SymptomCheck
Nimiq provider not found on window.nimiqOpen the app inside Nimiq Pay or install simulator helpers for local development.
Invalid Nimiq addressPass a normalized NQ address or let the helper read the selected account from the provider.
Verify fails after signingKeep appName, endpointPrefix, and request origin aligned between server and client.
Later API calls are unauthenticatedSend the Better Auth cookie or the stored token from set-auth-token.
Copyright © 2026