btxqid:// connect since v0.27Integrate Sign in with qID
Post-quantum wallet login for your BTX app in one sitting. One server middleware, one button. The user proves control of a BTX address and that address is their account — no email, no password, no relay server, and a login proof that cannot move funds, by construction.
The release pack is everything you need in one
zip: the server SDK, the widget, runnable Express / Next.js / static-site
examples, the test signer, and an INSTALL.md your developer (or
their AI assistant) follows to wire it in — about an hour, most of it reading.
Prefer the terminal? Everything below works from the unzipped pack. Prefer
zero maintenance? Skip the download and use the hosted widget in step 04.
01 · How it works
- Your server issues a one-time challenge
{nonce, rp_origin, ts}bound to your exact origin. - The user's wallet shows them your site and signs the challenge with their post-quantum login key (ML-DSA-44) — by QR scan from a phone, copy-paste, or a
btxqid://deep link into the installed desktop wallet. - Your server verifies the proof with the frozen qID Sign-In v1 verifier, rebuilds the address from the proof, and the verified address becomes (or finds) the account. Registration and login are the same action.
- A signed HttpOnly session cookie keeps them signed in. Sessions, logout, and account-by-address are included.
There is no relay server anywhere, unlike WalletConnect: your own backend is the rendezvous. The QR carries a proof URL on your server; the phone posts the proof there and the browser that showed the QR claims the session with a secret only it holds.
02 · Get the code
# download the release pack (button above), then:
unzip qid-connect-latest.zip && cd qid-connect-*
bun install
bun test packages # pins, frozen v1 vectors, e2e, QR + rotation flows
bun examples/express-demo/server.js
open http://localhost:8788 # a complete working login site, running on your machine
Everything is MIT. Two pieces matter: packages/server
(@qid/connect-server — challenge, verify, accounts, sessions, SQLite stores)
and packages/widget (@qid/connect-widget — the button, dialog,
and account chip, one vanilla JS file, zero dependencies). Vendor them or
workspace them; the examples show both ends wired.
During the private-preview window we ship the release pack as a download (the button above) rather than a public repo — its contents are identical to the repo, so everything on this page applies unchanged. Prefer git? Ask through qid.dev for read access.
03 · Wire your server
Express is one middleware line:
import express from "express";
import { createQidConnect } from "@qid/connect-server";
import { qidMiddleware, requireQidSession } from "@qid/connect-server/express";
const qid = createQidConnect({
origin: "https://yourapp.com", // your exact web origin
sessionSecret: process.env.QID_SESSION_SECRET, // 32+ random chars
});
const app = express();
app.use(express.json());
app.use("/qid", qidMiddleware(qid)); // the whole login system
app.get("/api/me", requireQidSession(qid), (req, res) => {
res.json(req.qidSession); // { address, account }
});
Next.js is one catch-all route file (see examples/next-demo/):
// app/api/qid/[action]/route.js
import { createQidConnect } from "@qid/connect-server";
import { qidNextHandlers } from "@qid/connect-server/next";
const qid = createQidConnect({
origin: process.env.QID_ORIGIN,
sessionSecret: process.env.QID_SESSION_SECRET,
apiPath: "/api/qid",
});
export const { GET, POST } = qidNextHandlers(qid);
04 · Mount the button — and the account chip
<div id="signin"></div> <!-- the Sign in button -->
<div id="account"></div> <!-- the signed-in chip (put it in your header) -->
<script type="module">
import { mountQidConnect, mountQidAccount } from "/qid-connect-widget.js";
// The signed-in state: a chip (green dot + address) that opens a dropdown
// with copy-address and a two-step Log out. Renders nothing when signed out.
const account = mountQidAccount(document.getElementById("account"), {
api: "/qid", // "/api/qid" on Next
onSignedOut() { location.reload(); },
});
mountQidConnect(document.getElementById("signin"), {
api: "/qid",
appName: "Your app",
onSignedIn() { account.refresh(); } // reveal the chip, no reload needed
}); // dialog opens QR-first by default
</script>
That's the complete login experience: the dialog is dynamic out of the box (countdown, self-rotating requests, server-side burn — a screenshotted QR is worthless minutes later), and the account chip keeps the signed-in state consistent on every BTX app: full address click-to-copy, extensible menu items, and a deliberate two-step Log out. Nothing to configure.
Prefer zero maintenance? Import the hosted widget instead of vendoring it:
import { mountQidConnect } from "https://qid.dev/connect/widget.js"
— wallet-list and UX updates then reach your users automatically when we
deploy, while the signed protocol underneath stays frozen v1. A
version-pinned copy (widget-<version>.js) exists for teams
that want to review every change first.
05 · Test without a wallet
The repo ships the frozen v1 reference signer as a CLI, so you can fake a
wallet during development. Click your button, copy nonce and
ts from the shown request, then:
bun tools/signer/btx-sign-ownership.mjs --random \
--origin http://localhost:8788 --nonce <nonce> --ts <ts>
Paste the printed proof into the dialog's desktop tab — or POST it to
/qid/proof to simulate a phone scanning the QR; the open dialog
signs itself in within a second or two. With a real
PQ Wallet for BTX (v0.21+): Settings → qID
Sign-In, or one click via the deep link on v0.27+.
06 · What your server now speaks
| route | what it does |
|---|---|
POST /qid/challenge | fresh one-time challenge bound to your origin (+ poll secret for QR); optional { retire } burns a superseded nonce on rotation |
POST /qid/verify | verifies a pasted proof, creates/finds the account by address, sets the session cookie |
POST /qid/proof | a remote signer (phone wallet) submits its proof; gets ok or a reason, never a session |
GET /qid/poll | the browser that issued the challenge claims the session once the remote proof lands |
GET /qid/session | { address, account, expiresAt } for the current session, else 401 |
POST /qid/logout | clears the session cookie |
07 · The security you inherit
- A login proof can never move funds. Sign-in signs under the
BTX-qID/login-v1domain tag; transactions sign underTapSighash. The domains cannot collide. Structural, not policy. - Origin-bound. The wallet shows the user your origin and signs exactly it; a proof for another site is worthless on yours.
- Single-use, short-lived, rotating. Nonces are consumed atomically, expire in minutes, and rotation burns whatever left the screen.
- Phishing-resistant QR. Conforming signers refuse any request whose proof URL is not the same origin as the site being shown.
- Login-CSRF guarded. A cross-site page cannot plant a session in a visitor's browser; both
/verifyand/pollcheck. - No third-party infrastructure. Nothing to go down, nothing to trust, nothing to subpoena.
Claims we make and none we do not: resistant to quantum attacks, built on NIST-standardized post-quantum cryptography (FIPS 204 ML-DSA-44), pending independent audit. Nothing here is "unhackable" and we never say so.
08 · Wallets your users can bring
09 · The fine print
The v1 proof format and verifier are frozen; future capability arrives as
new versions verified alongside v1, so a login you ship today keeps working
forever. Phase 1 is deliberately login-only: no transaction requests, no
permissions, no persistent connections — that is what makes it safe to adopt
now (later phases are gated behind an independent audit). The code is MIT;
the qID name and mark are not — forks take their own name. Keep the button
and dialog stock (docs/DESIGN.md) so "Sign in with qID" looks
the same on every BTX app, and never proxy or rewrite the challenge.