Getting Started

Architecture

Place provider access, auth, cross-device approval, utilities, and tests in the right runtime.

The SDK separates runtime access from auth decisions. Browser packages ask providers to do work. Server packages verify proofs and create Better Auth sessions.

Runtime access stays in the browser

@onmax/nimiq-mini-app-kit waits for provider surfaces and validates their shape.

  • window.nimiq is the Nimiq mini-app provider.
  • window.ethereum is the EIP-1193 provider surface.
  • EIP-6963 helpers announce and discover Ethereum providers.
  • Development helpers install simulated providers when the real host is not available.
app/runtime.ts
import { initMiniAppEthereumProvider, initMiniAppProvider } from '@onmax/nimiq-mini-app-kit'

export async function detectMiniAppRuntime() {
  const [nimiq, ethereum] = await Promise.allSettled([
    initMiniAppProvider({ timeout: 2_000 }),
    initMiniAppEthereumProvider({ timeout: 2_000 }),
  ])

  return {
    nimiq: nimiq.status === 'fulfilled',
    ethereum: ethereum.status === 'fulfilled',
  }
}

Use this when a route can render a "connect from Nimiq Pay" state before asking the wallet to sign.

Do not call provider waits from Better Auth server plugins, Nitro server routes, or Cloudflare Workers.

Direct auth owns same-device sessions

@onmax/better-auth-nimiq owns direct sign-in.

  1. The server issues a nonce and stores it with a TTL.
  2. The client asks the Nimiq provider to sign the challenge.
  3. The server verifies the public key, signature, address, and origin.
  4. Better Auth creates the session.
sequenceDiagram
  participant Browser
  participant NimiqPay as "window.nimiq"
  participant Auth as "Better Auth"
  Browser->>Auth: POST /nimiq/nonce
  Auth-->>Browser: nonce, message, expiry
  Browser->>NimiqPay: sign(message)
  NimiqPay-->>Browser: publicKey, signature
  Browser->>Auth: POST /nimiq/verify
  Auth-->>Browser: session cookie + token

Use this when the browser that starts the flow can access window.nimiq.

Do not use it when the approval wallet lives on a second device.

Cross-device approval owns handoff state

@onmax/better-auth-cross-device owns the order lifecycle. @onmax/cross-device-nimiq supplies the Nimiq adapter.

  1. Desktop starts an order and receives a claim URL plus desktop token.
  2. Phone claims the order and receives a challenge token.
  3. Phone signs the challenge or transaction proof.
  4. Desktop finalizes the order with the desktop token.

For login orders, the desktop receives the Better Auth session. For sign and transaction orders, the desktop receives the proof artifact.

server/auth.ts
import { crossDevice } from '@onmax/better-auth-cross-device'
import { createNimiqCrossDeviceAdapter } from '@onmax/cross-device-nimiq/server'

crossDevice({
  endpointPrefix: '/cross-device',
  adapters: [
    createNimiqCrossDeviceAdapter({ appName: 'Arcade Rewards' }),
  ],
})

Use this when you need a QR, deep link, or second-screen approval.

Do not store the desktopToken in the phone flow. It belongs to the device that started the order.

Utilities stay small

@onmax/unimiq provides a runtime-aware Nimiq facade. In Node.js and Cloudflare Workers, use an RPC URL because direct @nimiq/core loading is not used there.

@onmax/unerc20 talks to ERC-20 contracts through any EIP-1193 provider.

@onmax/better-auth-ledger stores app-local virtual balances. It is not a payment settlement layer.

test/nimiq-balance.test.ts
import { createNimiq } from '@onmax/unimiq'
import { createMockNimiqDriver } from '@onmax/unimiq/mock'

const nimiq = createNimiq({
  driver: createMockNimiqDriver({
    accounts: {
      sender: { balance: 5_000_000n },
    },
  }),
})

console.log(await nimiq.balance('sender'))

Expected output:

5000000n

Use mock drivers for deterministic unit tests. Use RPC-backed drivers when server or Worker code needs real network data.

Copyright © 2026