Device intelligence in 10 minutes
Add the glassprint engine to your backend and it auto-mounts every endpoint and runs the full identification pipeline in-process — no separate server to deploy. Then add the browser client, call identify(), and read a typed verdict: human, declared agent, undeclared automation, or unknown.
How the flow fits together
glassprint is server-authoritative (Archetype B): the browser client only reads signals behind a fail-closed consent gate and seals them to youringest endpoint. The embedded engine in your backend decrypts, adds network-tier signals the browser can't see, resolves identity, and returns the typed result. The visitorId is generated in-process and is tenant-bound — it is never computed in the page.
Embed the engine
Add one library dependency. The engine auto-mounts all glassprint endpoints — ingest, pubkey, me, privacy-rights — and manages storage and verdict processing in-process. No separate server to run.
Collect & seal
The MIT client reads each probe whose purpose your consent engine has granted, seals the bundle (AES-256-GCM, or HPKE/ECDH-P256), and POSTs it to your own ingest endpoint.
Act on the result
Your backend already has the typed verdict in-process. Read class, visitorId, and suspectScore — allow, step-up, or review.
Embed the glassprint engine in your backend
The primary integration model: add the embeddable engine library to your own backend. It ships for Node (Express / Fastify), Go (net/http), Python (FastAPI / Flask), and Java (Spring Boot starter + framework-agnostic core). Adding the dependency auto-provides the ingest, pubkey, me, and privacy-rights endpoints plus storage and verdict processing — all in-process.
Node — Express
npm i @noidme/glassprint-engine express
import express from 'express';
import { createEngine } from '@noidme/glassprint-engine';
import { glassprintEngine } from '@noidme/glassprint-engine/express';
const engine = await createEngine({
storage: { driver: 'sqlite', path: './glassprint.db' }, // or 'postgres'
kids: ['acme'],
masterSecret: process.env.GLASSPRINT_MASTER_SECRET,
});
express().use(glassprintEngine(engine)).listen(8080);
// Now serving: GET /_fp/pubkey POST /_fp/i/v1 POST /me POST /privacy/*Node — Fastify
import Fastify from 'fastify';
import { createEngine } from '@noidme/glassprint-engine';
import { glassprintFastify } from '@noidme/glassprint-engine/fastify';
const engine = await createEngine({ storage: { driver: 'sqlite', path: './glassprint.db' }, kids: ['acme'] });
const app = Fastify();
app.register(glassprintFastify, { engine });
await app.listen({ port: 8080 });Go
go get github.com/noidme/glassprint-engine-go
import (
glassprint "github.com/noidme/glassprint-engine-go"
"net/http"
"log"
)
eng, err := glassprint.New(glassprint.Config{
MasterSecret: os.Getenv("GLASSPRINT_MASTER_SECRET"),
Kids: []string{"acme"},
})
if err != nil { log.Fatal(err) }
http.Handle("/", eng.Handler()) // auto-mounts all glassprint endpoints
log.Fatal(http.ListenAndServe(":8080", nil))Python — FastAPI
pip install "glassprint-engine[fastapi]"
from fastapi import FastAPI import glassprint_engine as gp engine = gp.create(storage="sqlite:///glassprint.db", kids=["acme"]) app = FastAPI() app.include_router(gp.asgi.router(engine)) # uvicorn main:app
Java — Spring Boot starter
Add the starter dependency to your pom.xml and set two properties. Every endpoint is auto-registered via Spring Boot auto-configuration — zero manual wiring.
<dependency> <groupId>com.noidme</groupId> <artifactId>glassprint-engine-spring-boot-starter</artifactId> <version>0.1.0</version> </dependency>
glassprint.master-secret=your-at-least-32-char-production-secret glassprint.kids=acme # optional: glassprint.jdbc-url=jdbc:postgresql://localhost/glassprint
Install the browser client
The browser client is @noidme/glassprint— MIT-licensed, ships ESM + CJS + types, and stays under a 30 KB gzipped budget. You can audit exactly what it collects because the collection code runs in your page.
npm i @noidme/glassprint
Point the client at the endpoint your embedded engine is now serving — your own domain, no external dependency.
import { createGlassprint, createSealedTransport } from '@noidme/glassprint';
const transport = createSealedTransport({
key, // your tenant CryptoKey, or use HPKE: fetch /_fp/pubkey
deliver: async (sealed) =>
(await fetch('https://id.acme.com/_fp/i/v1', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(sealed), // { kid, iv, ct, epk? }
})).json(),
});
const gp = createGlassprint(
{ kid: 'acme', endpoint: 'https://id.acme.com/_fp/i/v1' },
transport,
);createGlassprint has no transport, so identify() resolves with reason: 'transport-error' and visitorId: null — it never mints an id until you wire delivery. A working integration must supply a transport that seals the payload and POSTs it to your endpoint.Call identify()
identify() always resolves and never throws — on consent denial, a missing engine, no signals, or a transport failure it still returns a typed result you can branch on. The optional tag is free-form and echoed back so you can correlate the call to a place in your flow (e.g. checkout, login).
const r = await gp.identify({ tag: 'checkout' });Read the typed result
The result is a verdict, not a bare id. Read classto know what you're looking at, visitorId for the device, and suspectScore as additive risk data. class is one of four values:
| class | Meaning |
|---|---|
human | Assessed as a real person — above the confidence floor, no human-implausible picture. |
declared_agent | A bot that declared itself and verified via Web Bot Auth (RFC 9421 Ed25519). Declared is recorded, but still scored — authenticated is not the same as trusted. |
undeclared_automation | Automation that did not declare itself, with a human-implausible signal picture (e.g. multiple strong tells). Never emitted at high confidence behind a CDN. |
unknown | Below the confidence floor — the server refuses to guess. A lone signal (one webdriver flag, one software GPU) lands here, which is also how privacy users are protected from a false bot label. |
The companion fields:
| Field | Type | What it is |
|---|---|---|
visitorId | string | null | Engine-minted, salted-HMAC, epoch-rotated, and tenant-bound (the same device is distinct per customer — no cross-customer supercookie). null when no id could be minted (e.g. transport-error, consent-denied). |
suspectScore | number? | Additive risk in [0,1] from inconsistencies and automation tells. Data for your risk engine — never a verdict to act on blindly. A recognised privacy defence (Brave/Tor farbling) caps it. |
reason | IdentifyReason | ok · consent-denied · consent-engine-absent · no-signals · transport-error — branch on this before trusting visitorId. |
const r = await gp.identify({ tag: 'checkout' });
if (r.reason !== 'ok') {
// consent-denied | consent-engine-absent | no-signals | transport-error
// r.visitorId is null — fall back to your unauthenticated path.
} else if (r.class === 'undeclared_automation') {
// additive risk signal — step up or review
} else {
// r.class is 'human' | 'declared_agent' | 'unknown'
// The verdict is already in your backend in-process — act on it directly.
send(r.visitorId, r.class, r.suspectScore);
}Act on the verdict — it is already in your backend
Because the engine runs in-process, the verdict is available server-side the moment the ingest request is processed — no round-trip required. Your risk logic can act on it directly from the same process. The ingest surface returns the same typed payload over HTTP, with status codes you can key on:
| Status | Reason | Meaning | |
|---|---|---|---|
| POST | 200 | ok | Identified — act on the verdict. |
| POST | 451 | consent-missing | No lawful basis to identify — the device-identification purpose was not granted. Lawful by construction. |
| POST | 409 | replay | The replay nonce was already claimed. |
| POST | 429 | rate-limited | Back off — a Retry-After header is set. |
On your backend, treat the verdict as one input among your signals:
// Your backend — the verdict arrives via the embedded engine in-process:
async function onCheckout(req) {
const { class: klass, visitorId, suspectScore } = req.body;
if (klass === 'undeclared_automation') return stepUp(); // challenge
if (suspectScore && suspectScore > 0.6) return review(); // hold for review
if (klass === 'declared_agent') return agentPath(); // a known good bot
return allow(visitorId); // human / unknown above your own threshold
}glassprint-server (Docker / Helm / Terraform) is the alternative for polyglot shops or teams that prefer a separate service. The trained-ML fuzzy linker is roadmap — the live identity path is exact-match. The cross-browser deviceId field is reserved and returns null today. The managed-SaaS option is design-partner onboarding, not open self-signup.The ten-minute path
- Add
@noidme/glassprint-engine(Node),glassprint-engine-go(Go),glassprint-engine[fastapi](Python), or the Spring Boot starter (Java) to your backend. - The engine auto-mounts all glassprint endpoints (ingest, pubkey, me, privacy-rights) in-process — no separate server.
- Install
@noidme/glassprint(browser client, MIT). - Create the client with your
kidandendpoint, and wire a sealed transport (without it,identify()returns transport-error /visitorId: null). - Call
await gp.identify({ tag })behind your consent engine. - Read
r.class,r.visitorIdandr.suspectScore— the verdict is already in your backend in-process.
Run it inside your own perimeter
Embed the glassprint engine in your own backend so your fraud signals never leave your network, then feed the typed verdict into the risk engine you already run.