SignetSignet

Quickstart

From nothing to a signature. This assumes you have created an app in the console and deployed a signing group — if you have not, do that first; it takes a few minutes.

1. Install the SDK

shell
npm install @oleary-labs/signet-sdk viem

Client-side zero-knowledge proving additionally needs @noir-lang/noir_js, @aztec/bb.js, and @oleary-labs/signet-circuits. They are optional peer dependencies — skip them if you prove server-side.

2. Authenticate the user

The user signs in with OAuth. Their token never reaches the network: a zero-knowledge proof establishes that they hold a valid credential from an issuer your group trusts, bound to an ephemeral session key.

signIn.ts
import { startGoogleOAuth, decodeIdToken, handleOAuthCallback } from "@oleary-labs/signet-sdk/oauth";
import { generateSessionKeypair } from "@oleary-labs/signet-sdk/session";
import { generateJWTProof, getJWTModulusBytes } from "@oleary-labs/signet-sdk/proof";
import { authenticateWithBootstrap } from "@oleary-labs/signet-sdk/bootstrap";

// Kick the user to Google.
await startGoogleOAuth({ clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID! });

// …and back on your callback route:
const jwt = await handleOAuthCallback("/api/auth/token");
const claims = decodeIdToken(jwt);
const keypair = await generateSessionKeypair();

// The slow step — a few seconds, once per sign-in.
const { proof } = await generateJWTProof(jwt, keypair.publicKeyHex);
const modulus = await getJWTModulusBytes(jwt);

await authenticateWithBootstrap(
  { groupId: GROUP_ADDRESS, nodeUrls: NODE_URLS },
  proof,
  keypair.publicKeyHex,
  claims,
  modulus,
);

Why the proof exists

Forwarding the raw token would put a replayable credential on every node in your group. The proof establishes the same fact — this person holds a valid token from a trusted issuer — while leaving nothing behind that anyone could reuse.

3. Create the user’s key

Key generation is a distributed protocol: every operator ends up with one share, and no machine anywhere ever holds the whole key.

keygen.ts
import { keygen } from "@oleary-labs/signet-sdk/keygen";

const key = await keygen(
  { groupId: GROUP_ADDRESS, nodeUrls: NODE_URLS },
  keypair,
  claims,
);

console.log(key.ethereumAddress); // the user's address
console.log(key.alreadyExisted);  // true on every sign-in after the first

4. Sign something

sign.ts
import { signSignRequest } from "@oleary-labs/signet-sdk/request";

const signed = await signSignRequest(keypair, claims, GROUP_ADDRESS, messageHash);

const res = await fetch(`${NODE_URLS[0]}/v1/sign`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ ...signed, curve: "frost_secp256k1" }),
});

const { ethereum_signature } = await res.json();

You contact one node. It coordinates the round with the rest of the group over their own peer-to-peer mesh, and returns when a quorum has contributed. Every participant independently re-checks your session signature before it takes part — the node you happened to call cannot vouch for you to the others.

5. Constrain what the key may sign

A key with no scope will sign any hash you hand it. For anything an agent or a background job touches, mint a scoped sub-key instead — bound to one chain, one contract, and one message type, enforced by every operator.

Running it all locally

shell
# Terminal 1 — Anvil, contracts, three nodes, a bootstrap group
cd signet-protocol && devnet/start.sh

# Terminal 2 — the bundler and paymaster
cd signet-min-bundler && scripts/devnet-setup.sh && ./bundler --config bundler.toml

# Terminal 3 — this platform
cd platform/backend && go run ./cmd/server
cd platform/web && npm run dev