Wallets for your users
Give your users Tenzro identities and wallets, with their agents and machines, inside your app. Passkeys stay on each user's device: neither your app nor Tenzro Labs can sign for them. Part of the developer subscription.
How it works
- A user signs in or creates a wallet with a passkey, in the open Tenzro wallet. Their identity,
did:tenzro:human:…, is derived from that passkey. - Anything that changes what can happen with their wallet (a new agent, new limits, an action held for them) is approved in the Tenzro Labs approval page, inline in your app or in a window, with a Labs Wallets passkey the user links to their identity once, approved from a device already on it. Linking is available with Network 1; until then users approve in the open wallet.
- Agents act only within Terms the user approved, enforced by every node: budgets, payees, models, and step-up above a threshold.
- Machines join with one command run on the machine; its TPM or Secure Enclave key is its identity.
tenzro CLI. Agents rooted in them stay under their control.1. Keys
Your backend calls Wallets with a Platform API key, the same tzl_… key that calls every other Tenzro Labs service. On the API keys page, give it wallets:read to read, or wallets:write to request changes (it includes read). Keys issued to agents may hold them too. Wallets checks each key with the Platform, so revoking it or removing a scope takes effect within 30 seconds.
In Wallets, create a project and list your app's origins. The project's publishable key (pk_…) is for the browser embed only and is accepted only from those origins.
| Credential | Where | For |
|---|---|---|
tzl_… | Your servers | Every server call to Wallets, with wallets:read or wallets:write |
pk_… | The browser | The embed: sign-in, wallet creation and approvals, from your listed origins |
Which org and project
A key acts for the Wallets org where its account is an admin or owner. If the account administers several, name one with the header Tenzro-Wallets-Org: org_…; without it Wallets answers 409 org_required. A call about one project's users names it with Tenzro-Wallets-Project: prj_…. Usage is recorded against the org and attributed to the key.
2. Sign users in
import { TenzroWallets } from "@tenzrolabs/wallets";
const wallets = new TenzroWallets({ publishableKey: "pk_..." });
// From a click: the Tenzro wallet opens, the user picks their passkey
// (or creates one, which creates their identity and wallet).
const user = await wallets.signIn({ externalId: currentUser.id });
// Hand the short-lived user token to your backend.
await fetch("/api/tenzro/session", { method: "POST", body: user.userToken });On your backend
import { TenzroLabs } from "@tenzrolabs/platform/server";
// Your backend, with a Platform API key that holds wallets:write.
const res = await fetch(`https://wallet.tenzro.xyz/api/v1/users/verify?token=${encodeURIComponent(userToken)}`, {
headers: {
authorization: `Bearer ${process.env.TENZRO_LABS_API_KEY}`, // tzl_...
"tenzro-wallets-project": "prj_...",
},
});
const user = await res.json(); // { did, account, external_id }
// Propose an agent for that user with the Platform SDK. Nothing exists until the user approves it.
const labs = new TenzroLabs({ wallets: { project: "prj_..." } }); // reads TENZRO_LABS_API_KEY
const agent = await labs.wallets.agents.create({
root_did: user.did,
name: "assistant",
template: "inference",
limits: { daily: "20", per_tx: "1", step_up_above: "5" },
});
// agent.approval_id -> show <ApproveRequest> to the user3. Approvals in your UI
import { TenzroWalletsProvider, SignInWithTenzro, ApproveRequest } from "@tenzrolabs/wallets/react";
<TenzroWalletsProvider publishableKey="pk_...">
<SignInWithTenzro onSignedIn={(u) => startSession(u.userToken)} />
<ApproveRequest approvalId={approvalId} userToken={userToken} inline onDone={refresh} />
</TenzroWalletsProvider>inline renders the approval page in an iframe with passkeys delegated (allow="publickey-credentials-get; publickey-credentials-create"). Where the browser does not support passkeys in cross-origin iframes, leave it out and the approval opens in a window.
4. Held actions
// Your agent runtime: the node held an action for the user (Terms step_up_above).
try {
await node.agentAct(action);
} catch (e) {
if (e.data?.step_up) {
const { id } = await labs.wallets.approvals.requestStepUp({ agent_did, action, step_up: e.data.step_up });
// The user approves in your app (<ApproveRequest approvalId={id} />).
// Then, from the approval.approved webhook or approvals.get(id):
const { step_up } = await labs.wallets.approvals.get(id);
await node.agentAct({ ...action, step_up });
}
}5. From your server, with the Platform SDK
@tenzrolabs/platform calls Wallets with the same key: the org, members, agents (list, get, propose, limits, pause, resume, revoke), approvals, webhooks, headless wallets and usage. Each change returns an approval_id for a person to approve with their passkey; nothing here can sign for anyone.
import { TenzroLabs } from "@tenzrolabs/platform/server";
const labs = new TenzroLabs({ wallets: { org: "org_..." } }); // org only if your account administers several
await labs.wallets.org.get();
await labs.wallets.members.list();
const agents = await labs.wallets.agents.list();
await labs.wallets.agents.limits(agents[0].did, { daily: "50" }); // -> { approval_id }
await labs.wallets.approvals.list({ status: "pending" });
await labs.wallets.webhooks.add({ url: "https://example.com/hooks/tenzro", events: ["approval.requested"] });
await labs.wallets.headless.list();
await labs.wallets.usage();Without touching passkeys
Your code never calls WebAuthn. Every passkey step runs on the Tenzro Labs ceremony page, in a window or inline where the browser allows, and the SDK returns the result.
| Browser SDK (@tenzrolabs/wallets) | What the person sees |
|---|---|
createWallet() / signIn() | A passkey created, or used, on their device; you get the user and their wallet address |
approve(id) / signMessage(msg) | One passkey prompt |
devices.list() / devices.remove(id) | Their devices |
devices.link() / devices.join(code) | A QR code or an 8-character code on the existing device; the new device joins once approved there |
recovery.addEmail(email) | A recovery contact for notices, never a signing factor |
recovery.setCode() / recovery.start() | A recovery code; recovery on a new device, approved by a linked device, recovery signers or the code after a delay |
React components from @tenzrolabs/wallets/react wrap the same calls: CreateWallet, SignInWithTenzro, ApproveRequest, LinkDevice, JoinDevice, DeviceList, RecoverWallet and RecoveryEmail. Linking and removing devices, recovery signers, recovery codes and recovery itself change the keystore on chain: they are available with Network 1, and until then the SDK answers switch_on_pending.
Headless wallets
A headless wallet is for your servers and services: a machine identity whose key is generated in that machine's TPM or Secure Enclave, controlled by a person in your org through their passkey, and able to sign unattended only within the Terms that person approved once.
- Hardware keys only. The key is made and kept in the machine's TPM 2.0 or Secure Enclave and never leaves it. There is no software key and no fallback. Cloud vTPMs work: AWS NitroTPM, Google Cloud Shielded VM and Azure Trusted Launch.
- Created on the machine.
headless.createruns on the machine itself, with the Wallets server SDK, because it needs the local TPM or Secure Enclave. It attests the key and sends the request; nothing is sent from a host without one. - Approved by the root. The request becomes an approval for the person who will control the wallet. When they approve with their passkey, the machine identity is registered with them as its controller, with its Terms. Wallets sends
headless.requested,headless.key_requested,headless.suspended,headless.resumedandheadless.revokedwebhooks. - Signing stays on the machine. Sends and signatures are made by the hardware key; the network enforces the Terms. Wallets never holds a signature it could replay.
- Rotation and a second machine. Every new hardware key proves possession of a one-time challenge and is endorsed by one of the wallet's current keys, so an API key alone cannot attach a machine; the root approves each key.
- Revocation. The root, or a
wallets:writekey, suspends a headless wallet at once in Wallets; the revoke on chain is a passkey approval. Lists come from Wallets; balances are read from chain once the wallet is registered.
// On the machine that will hold the key, with the Wallets server SDK and your Platform key (wallets:write).
import { TenzroWalletsServer } from "@tenzrolabs/wallets/server";
const wallets = new TenzroWalletsServer({ apiKey: process.env.TENZRO_API_KEY!, org: "org_..." });
// The key is made in this machine's TPM or Secure Enclave under "settlement-worker".
const wallet = await wallets.headless.create({
keyLabel: "settlement-worker",
label: "Settlement worker 1",
limits: { daily: "100", per_tx: "10" },
});
// wallet.state === "awaiting_root": the root approves the wallet and its limits with their passkey.
// A second machine: make its key there, endorse it from a machine already in the wallet.
const request = await wallets.headless.prepareLink(wallet.id, { keyLabel: "settlement-worker", host: "worker-2" }); // on worker-2
await wallets.headless.approveLink(request, { keyLabel: "settlement-worker" }); // on worker-1
// Rotate this machine's key; the old key endorses the new one.
await wallets.headless.rotate(wallet.id, { keyLabel: "settlement-worker", newKeyLabel: "settlement-worker-2" });On a host with no TPM or Secure Enclave, creation stops on the machine with:
Available with Network 1
Three network features complete headless wallets: spending Terms on machine accounts enforced by consensus, hardware attestation graded when a machine identity registers, and keystore entries for a second machine key and for rotation. Until they are live, Wallets records, shows and routes the approvals, and unattended sends answer switch_on_pending.
Webhooks
Add an HTTPS endpoint in Wallets. Each delivery is signed with ES256 by a hardware key (TPM or Secure Enclave) of the instance that sent it; verify with verifyWebhook from @tenzrolabs/wallets/server against https://wallet.tenzro.xyz/.well-known/jwks.json.
| Event | When |
|---|---|
approval.requested | A change or a held action waits for a user's passkey |
approval.approved | Approved; for a held action it carries the step_up to send |
agent.active | The agent is on chain |
alert.spend | An agent passed the alert share of its daily budget |
headless.requested | A machine asked for a headless wallet; it waits for its root |
headless.key_requested | A second machine or a rotated key was endorsed and waits for the root |
headless.suspended | Wallets stopped acting for the wallet (headless.resumed when undone) |
headless.revoked | The wallet is revoked in Wallets |
Plans
Wallets is part of the developer subscription. Each plan includes monthly active wallets, agents and machines, with use above that drawn from your credits. Developer preview: no caps, usage counted (GET /api/v1/usage).