The Passkey Manager Program is the core authorization program for passkey-backed flows on Thru.
From a developer perspective, the key idea is that the passkey does not sign the outer Thru transaction directly. Instead, the program verifies a WebAuthn signature over a challenge built from the wallet nonce, the ordered account list, and the trailing instruction bytes that the program action is authorizing.
Use This When
- you need passkey-backed authorization for a transfer, CPI flow, or other nonce-bound program action
- you need to reason about authority records, credential lookup accounts, or nonce advancement
- you are integrating WebAuthn or native passkey signatures into a Thru transaction flow
Quickstart
Fetch the ABI, generate TypeScript builders, then build instruction bytes and send the outer transaction.
thru-cli abi account get --include-data --out ./passkey-manager.abi.yaml <ABI_ACCOUNT_ADDRESS>
thru-cli abi codegen \
--files ./passkey-manager.abi.yaml \
--language typescript \
--output ./generated
import { decodeAddress } from "@thru/helpers";
import type { Thru } from "@thru/thru-sdk/client";
import {
PasskeyInstruction,
TransferArgs,
} from "./generated/thru/program/passkey_manager/types";
const instructionData = PasskeyInstruction.builder()
.payload()
.select("transfer")
.writePayload(
TransferArgs.builder()
.set_wallet_account_idx(2)
.set_to_account_idx(3)
.set_amount(1_000_000)
)
.finish()
.build();
const tx = await thru.transactions.build({
feePayer: { publicKey: decodeAddress(feePayerAddress) },
program: "taUDdQyFxvM5i0HFRkEK3W45kWLyblAHSnMg4zplgUnz6Z",
accounts: {
readWrite: [walletAddress, destinationAddress],
readOnly: [],
},
instructionData,
});
const signedWire = await signTransaction(tx.toWireForSigning());
const signature = await thru.transactions.send(signedWire);
Real user-facing passkey flows usually prepend a generated validate instruction before the trailing program action. If you want the full validate-and-send path instead of raw generated builders, use @thru/passkey-manager.
The passkey proof and the outer transaction signature are different layers. A successful validate step authorizes the wallet action inside the program, but the outer transaction still needs to be built and submitted normally.
How It Works
Most passkey-managed flows follow this pattern:
- fetch the nonce from the on-chain wallet account
- build the trailing instruction payload you want the program to authorize
- hash
nonce || ordered account addresses || trailing instruction bytes
- sign that challenge with WebAuthn
- encode
validate
- concatenate
validate with the trailing instruction payload
- submit the resulting transaction against the Passkey Manager Program
This validate-then-execute model works whether the caller is a first-party wallet, a custom web app, or a mobile/backend integration.
Account Model
| Account | What it stores | Notes |
|---|
WalletAccount | Wallet header with num_auth and nonce | In practice, wallet data also includes trailing 65-byte authority records after the fixed header. |
CredentialLookup | Wallet pubkey for a registered credential | Useful when a passkey needs a lookup account for registration or recovery flows. |
Authority Records
Wallet authority entries are 65-byte tagged records:
tag = 1: passkey authority, stored as P-256 x[32] + y[32]
tag = 2: pubkey authority, stored as pubkey[32] + padding[32]
Required Accounts
Passkey-manager instructions rely on stable account indices:
- the fee payer is index
0
- the Passkey Manager Program is index
1
- the wallet account must appear as a non-fee-payer account in the transaction
- trailing instructions reference accounts by their position in the final ordered account list
If you are using the Web SDK, buildAccountContext(...) handles this ordering and index lookup.
| Event | What it means |
|---|
wallet_created | A new passkey-managed wallet account was created. |
wallet_validated | A validate step succeeded and advanced the wallet nonce. |
wallet_transfer | The wallet transferred balance to another account. |
credential_registered | A credential lookup account was registered for the wallet. |
Instructions
| Instruction | Use it when | Notes |
|---|
create | Create a new passkey-managed wallet | Includes the initial authority and a state proof for the new wallet account. |
validate | Submit a WebAuthn proof for the trailing wallet action | Carries r, s, authenticatorData, and clientDataJSON. |
transfer | Move native balance from the managed wallet | Usually follows validate in the same transaction payload. |
invoke | Have the managed wallet call another program | This is the main path for wallet-controlled CPI flows. |
add_authority | Add another passkey or pubkey authority | Useful for multi-authority or recovery flows. |
remove_authority | Remove an existing authority by index | Use with care because authority ordering matters. |
register_credential | Create a credential lookup account | Used when you want a stable on-chain mapping for a credential. |
Integration Surfaces
The program stands on its own, but most developers will interact with it through one of these integration patterns:
- Use
@thru/passkey-manager when your app needs to build validate, transfer, invoke, or authority-management instructions directly.
- Use
@thru/passkey when you need the browser WebAuthn registration and signing layer that feeds signatures into passkey-manager transactions.
- Embedded-wallet integrations typically use the wallet or provider layer for the outer transaction while relying on passkey-manager for the inner authorization payload.
- Mobile apps can use native passkey surfaces to register a credential and produce the WebAuthn proof locally.
- A common mobile pattern is challenge-submit: fetch or construct the passkey-manager challenge, sign it on-device, then send the signature components and WebAuthn payload back to a backend or transaction service that assembles the final transaction.
On-Chain Link