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.
Current version: 1.7.2 ·
what changed ·
pin it: /connect/qid-connect-1.7.2.zip
The pack ships a skill for your AI assistant.
A developer who knows their own codebase is signed in with a real
post-quantum signature in 15 to 30 minutes. Drop
skills/qid-connect/ into your assistant, ask for qID sign-in,
and it wires the server, the button and the account model, then verifies the
result against your running app. Working agent-first? Everything your agent
needs, including a one-paragraph bootstrap prompt and the machine-readable
llms.txt, is at qid.dev/ai.
It carries what used to cost the afternoon: the four mistakes that break
sign-in with no error in the browser at all. A wrong
origin. A proof_url your users' phones 404 against,
which leaves desktop working so it reaches production unnoticed. Per-process
stores on a multi-instance deploy. A strict CSP that silently blocks the
button. The bundled checker finds all four in seconds:
bun skills/qid-connect/scripts/check-integration.mjs https://yourapp.com
Twelve checks against your live integration, no wallet and no keys, each failure
printing its fix. The skill is also
open source on
GitHub. The rest of the pack is unchanged: server SDK, widget, runnable
Express / Next.js / static-site examples, the test signer, and
INSTALL.md. 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
# prereq: bun 1.x (install at https://bun.sh)
# 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, two files, zero external 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? Open an issue on the public skill repo and ask 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 "https://qid.dev/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, one import: 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, nothing to update: wallet-list and UX improvements reach your users automatically when we deploy, while the signed protocol underneath stays frozen v1.
Want to review every change first? Pin a version
(https://qid.dev/connect/widget-<version>.js), or vendor
packages/widget/src/ from the pack and import
qid-connect-widget.js from your own server. Copy the whole
directory: the widget statically imports its bundled QR encoder from
./vendor/qrcode.mjs, so taking only the one file leaves the module
graph unresolved and the button never renders. Same API either way.
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 · Ship it with your AI assistant
The pack includes skills/qid-connect/, a skill your coding
assistant reads instead of guessing. It picks the right path for your stack,
wires the server and the button, gets the account model right, and then
verifies the result against your running app. Install it once:
cp -R skills/qid-connect .claude/skills/ # then just ask for qID sign-in
Any other assistant can take SKILL.md as the task brief. The skill
is open source,
so you can read every line of what it tells your assistant to do.
The checker inside it is worth running whether or not you use an assistant. Twelve checks against a live integration, no wallet and no keys, each failure printing its fix:
bun skills/qid-connect/scripts/check-integration.mjs https://yourapp.com
# or, for routes mounted elsewhere:
bun skills/qid-connect/scripts/check-integration.mjs https://yourapp.com --api /api/qid
It exists because the mistakes that cost integrators an afternoon produce
no error in the browser at all. The dialog just opens and never
completes. An origin that is not where users land. A
proof_url phones 404 against, which leaves desktop paste working
so it reaches production unnoticed. Per-process stores on a multi-instance
deploy, where the QR renders and then flips to expired. A strict CSP silently
blocking the button. Seconds to find, instead of an afternoon.
10 · 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.