Your current login is a line item.
Ours is infrastructure.
Replace your app login in two days. Users still tap Continue with Google, Apple, or email.
In the background they get a self-custodial wallet and a portable on-chain identity. 1,000 users free, gas sponsored, no lock-in.
Integrate bonuz login.
Everything on this page is what the SDK does today. Sign-in methods are email (one-time code), Google and Apple. Your project key starts with bz_ and comes from app.bonuz.market/developers.
The script tag
Two lines. Nothing to install.
Paste this into any HTML page. The widget loads from bonuz.id, renders the button inside the div in a shadow DOM (your CSS and ours never collide) and offers email, Google and Apple sign-in. There is no npm package to add today: the @bonuz/sdk on npm is an unrelated 0.1.0, and 2.x ships inside the bonuz monorepo until it is published.
<script type="module" src="https://bonuz.id/sdk/v1/bonuz.js" data-project="bz_…"></script>
<div data-bonuz-login></div>window.bonuz and the three events
Listen for the user, then fetch a token.
Everything the widget knows reaches your page through window.bonuz and three events on window. bonuz:ready fires once when the widget is up (also when the config fetch fails, so your page can degrade). bonuz:login carries the user and fires on a fresh sign-in and when a returning visitor is restored. bonuz:logout fires when the session ends.
// window.bonuz exists from the moment the script runs. login() and logout()
// called before the widget is up are queued; the getters answer at once.
window.addEventListener('bonuz:ready', (e) => {
// The widget is up. e.detail.project is your project name, or null if the
// config fetch failed. It does not mean anyone is signed in yet: a returning
// visitor's session is still being restored. Take the user from bonuz:login.
console.log('bonuz ready for', e.detail.project)
})
// Fires on a fresh sign-in AND when a returning visitor's session is restored
window.addEventListener('bonuz:login', async (e) => {
showSignedIn(e.detail.user)
const token = await window.bonuz.getFreshToken() // Promise<string | null>
await fetch('/api/session', { method: 'POST', body: JSON.stringify({ token }) })
})
window.addEventListener('bonuz:logout', () => showSignedOut())
// Also on window.bonuz:
// login() opens the sign-in (a full-page redirect, no popup)
// logout() ends the session
// getToken() sync: the current access token, or null
// getUser() sync: { id, handle, name, walletAddress }, or null
// (null for a returning visitor until the session is restored)React
The same login as a provider and a hook.
For teams building in React (inside the bonuz monorepo today, or once 2.x is on npm). LoginModal lives in @bonuz/sdk/auth/ui, not in /components. Pair ConnectButton with LoginModal (onLogin opens it); a ConnectButton alone signs in with Google only. Peer dependencies: react and react-dom 18 or 19, @web3auth/modal 11, ethers 5, viem 2, wagmi, @tanstack/react-query 5, zustand, framer-motion.
import { useState } from 'react'
import { BonuzAuthProvider, useBonuzAuth } from '@bonuz/sdk/auth/react'
import { LoginModal } from '@bonuz/sdk/auth/ui'
import { ConnectButton } from '@bonuz/sdk/components'
import '@bonuz/sdk/styles.css'
export function App() {
return (
<BonuzAuthProvider config={{ projectId: 'bz_…' }}>
<SignInButton />
<Profile />
</BonuzAuthProvider>
)
}
// Pair ConnectButton with LoginModal: on its own the button signs in with Google only
function SignInButton() {
const [open, setOpen] = useState(false)
return (
<>
<ConnectButton onLogin={() => setOpen(true)} />
<LoginModal isOpen={open} onClose={() => setOpen(false)} />
</>
)
}
function Profile() {
const { isAuthenticated, isLoading, user, getToken, error, errorCode } = useBonuzAuth()
if (isLoading) return null
// errorCode: project_not_found | origin_not_allowed | project_paused | challenge_not_found | account_deleted
if (error) return <p>{errorCode}: {error}</p>
if (!isAuthenticated || !user) return null
// user: { id, handle, name, walletAddress, email? }
// getToken(): Promise<string | null>, always fresh. `token` on the hook may be stale during recovery.
return <p>{user.handle ?? user.name} · {user.walletAddress}</p>
}Domains
Allow every origin you sign in from.
Open app.bonuz.market, go to Developers, open your project and add each origin under Domains. That includes http://localhost:3000 with the exact port you run on. Local addresses are approved instantly; real domains wait for bonuz approval. Until an origin is approved, sign-in on it shows a blocker message instead of the login flow.
Verify the token on your server
One GET to the bonuz API.
There is no public JWKS yet, so do not try to verify the signature yourself. Ask the bonuz API who the token belongs to. The caveat matters: a valid token proves a bonuz user, not a user of your app specifically, because every app shares one audience for now.
// Your server. The token arrived from the browser.
const res = await fetch('https://admin.bonuz.xyz/api/users/me', {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) return unauthorized()
const bonuzUser = await res.json()
// A 200 means: this is a real, signed-in bonuz user.
// It does NOT mean the token was issued to your app. Every app shares one audience today.
// Treat it as "a bonuz user", key your own records on bonuzUser.id, and never grant admin rights from it alone.CSP and COOP
The minimum your host page must allow.
If your site sends a Content-Security-Policy, these are the floors. The RPC hosts are the ones the bonuz wallet reads balances from; add any others your own code calls. Email, Google and Apple sign-in redirect to the sign-in page and back, so they work with any Cross-Origin-Opener-Policy. Only the qID passkey window, which only bonuz apps offer, needs an opener: plain same-origin would break it there, so same-origin-allow-popups (or no COOP) is the safe default.
Content-Security-Policy:
script-src 'self' https://bonuz.id https://*.web3auth.io 'unsafe-eval' 'wasm-unsafe-eval';
style-src 'self' https://bonuz.id; /* add 'unsafe-inline' only for very old browsers */
connect-src 'self' https://bonuz.id https://admin.bonuz.xyz https://*.web3auth.io wss://*.web3auth.io
https://mainnet.base.org https://sepolia.base.org https://*.publicnode.com; /* the wallet's RPCs */
frame-src https://*.web3auth.io;
img-src 'self' data: https:; /* sign-in logos */
Cross-Origin-Opener-Policy: same-origin-allow-popups /* safe default, or no COOP. Email, Google and Apple sign-in
redirect and work with any COOP; only the qID passkey window
(bonuz's own apps) needs an opener, which same-origin breaks */WordPress
Paste it as a Custom HTML block.
Use one Custom HTML block containing the script tag and the div, or a header/footer plugin that pastes raw HTML. Enqueueing the script from functions.php does not work: WordPress prints neither the type="module" (classic enqueue) nor the data-project attribute (module enqueue) the widget needs.
<!-- One Custom HTML block where the sign-in button should appear -->
<script type="module" src="https://bonuz.id/sdk/v1/bonuz.js" data-project="bz_…"></script>
<div data-bonuz-login></div>
<!-- Enqueueing from the theme does not work: wp_enqueue_script() drops
type="module", and wp_enqueue_script_module() prints no data-project, so the
widget never starts. A header or footer plugin that pastes raw HTML works
the same as the block above. -->What your app receives.
The script tag exposes id, handle, name and wallet address. The React hook also exposes the sign-in email. Sign-in itself happens on the sign-in provider's page (Web3Auth, Google or Apple), so one-time codes and passwords never touch your page.
Script tag
id, handle, name, walletAddress
The user object carries no email. Those four fields are what the host page gets.
React hook
id, handle, name, walletAddress, email (optional)
The React path also exposes the sign-in email on user.email. Store it only if you need it.
Your server
Whatever GET /api/users/me returns for that token
The same bonuz user record. Key your own data on the id; handles can change.
Already in production.
Real products built on the same SDK login.
What you ship now vs. what you ship with bonuz.
Same UX. Fundamentally different infrastructure underneath.
Old stack
Login as a line item
- ✕Email + password. Breach risk on you.
- ✕Magic link vendor lock-in.
- ✕No wallet. No on-chain identity. No reach beyond your app.
- ✕Loyalty is a database row, not a portable asset.
- ✕Seed phrases scare 95% of your users away.
bonuz SDK login
Login as infrastructure
- ✓Continue with Google, Apple, or email. Same UX your users already know.
- ✓Self-custodial wallet (MPC/TSS) provisioned silently in the background.
- ✓Portable bonuz ID: verified socials, attestations, and graph from day one.
- ✓Gas sponsored. Users never see a fee or a seed phrase.
- ✓No lock-in. Your users stay yours, but now they are also yours on-chain.
Three phases. Start shipping today.
Drop in the login first. Expand into engagement and the graph when you're ready.
Drop-in login
~ 2 daysReplace your existing auth with the bonuz SDK login button. Users keep the same flow. You get wallets, bonuz IDs, and session handling out of the box.
Activate engagement
~ 1 weekStart issuing DNFTs: loyalty cards, passes, vouchers, certificates. Gas sponsored. State machine handled by the protocol.
Plug into the graph
OngoingQuery the social graph, read cross-app attestations, and let users bring reputation in from every other bonuz integration.
Login vendor vs. bonuz SDK login
A side-by-side of what you pay for and what you get.
Live on-chain. Today.
Verifiable, audited, and already securing real users.
Identity registry
0x9220070245b67130977FdF32acA4acdF6aD163cC
Attestation protocol
0xe6461B0C86bF85023a5f359BAdF7012d2ef5ad7e
Engagement / DNFT core
0x7034625223C3fc8628214cd0504fdE7Cf5CEDA63
The obvious questions.
Answered before you ask.
Will this change how my users log in?
No. The button still says Continue with Google, Apple, or email. The wallet and bonuz ID are provisioned silently in the background.
Am I locking myself into bonuz?
No. The SDK is free, the protocol is open, and the user owns their keys. If you ever walk away, your users keep their wallet and identity.
Who pays gas?
You do, from a sponsored developer wallet. Your users never see a fee. Gas costs on our target chains are a fraction of a cent per action.
Is it secure?
MPC/TSS key management with MFA. Smart contracts audited by Hacken (10/10 security score). No single point of failure, no custody risk for you.
What does it cost?
The first 1,000 monthly active users of each app are free, whichever way they sign in. Above that, simple per-user pricing, shown in your Developers console. Gas you choose to sponsor is separate.
Swap your login today.
Keep your UX. Gain a self-custodial wallet, a portable on-chain identity, and a full engagement layer.