integration guide · phase 1: sign-in

Integrate 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

  1. Your server issues a one-time challenge {nonce, rp_origin, ts} bound to your exact origin.
  2. 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.
  3. 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.
  4. 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

routewhat it does
POST /qid/challengefresh one-time challenge bound to your origin (+ poll secret for QR); optional { retire } burns a superseded nonce on rotation
POST /qid/verifyverifies a pasted proof, creates/finds the account by address, sets the session cookie
POST /qid/proofa remote signer (phone wallet) submits its proof; gets ok or a reason, never a session
GET /qid/pollthe 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/logoutclears the session cookie

07 · The security you inherit

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

PQ Wallet for BTX · desktopavailable now: copy-paste since v0.21, one-click btxqid:// connect since v0.27
bonuz Wallet · mobileavailable now: scan the qID QR with the bonuz app, pick your wallet, and you are in
Building a wallet? The wallet-side signer SDK (request parsing, the normative signer rules, a reference mobile approval screen) is available to wallet builders: open an issue on the public skill repo.

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.