Keys & identity
The four-key identity an agent derives from a single signature — spending keys and viewing keys, and how they are derived.
A Hestia identity is four keys derived from one secret seed. Two govern spending, two govern viewing. An agent can recreate all four at any time from its wallet, so there is no new secret to store or back up.
The four keys
interface Keys {
sk: bigint; // secret spending scalar — never leaves the agent
SK: bigint; // public spending key, SK = poseidon([sk])
vk: Uint8Array; // X25519 secret — decrypts incoming notes
VK: Uint8Array; // X25519 public — notes are encrypted to this
}| Key | Kind | What it does |
|---|---|---|
sk | secret scalar | Authorizes spends. Produces nullifiers. Must stay on the device. |
SK | public field element | A note's owner. Senders set owner = SK so only you can spend. |
vk | X25519 secret | Trial-decrypts note ciphertexts to discover what you own. The unit of selective disclosure. |
VK | X25519 public | Senders encrypt note plaintext to this so only you can read it. |
The split is deliberate: you can hand someone your viewing secret to let them read your history without giving them any ability to spend. The spending secret never has to leave the device for any reason.
Deriving keys from a seed
All four come from a 32-byte seed through fixed, domain-separated derivations:
sk = poseidon([toField(seed), 0]) SK = poseidon([sk])
vk = keccak256(seed ‖ 0x01) VK = X25519(vk)import { deriveKeysFromSeed } from "@hestia/common";
const keys = await deriveKeysFromSeed(seed); // seed: Uint8Array (32 bytes)Deriving keys from a wallet signature
In practice an agent derives its seed from a signature over a fixed message, so the same wallet always reproduces the same Hestia identity:
import { deriveKeysFromSignature, KEY_DERIVATION_MESSAGE } from "@hestia/common";
import { hexToBytes } from "viem";
// KEY_DERIVATION_MESSAGE === "hestia.io/keys/v1"
const sig = await wallet.signMessage({ account, message: KEY_DERIVATION_MESSAGE });
const keys = await deriveKeysFromSignature(hexToBytes(sig));
// internally: seed = keccak256(signature), then deriveKeysFromSeed(seed)This is exactly what the Labs console does: one signature on connect, and the agent's shielded identity exists — nothing is generated randomly, nothing is stored server-side.
Because the identity is a deterministic function of the signature, use the canonical message. Signing a different message — or using a wallet that produces non-deterministic signatures for personal-sign — yields a different, unrecoverable identity. Hestia uses the versioned constant
hestia.io/keys/v1for exactly this reason.
From keys to a payable address
Your SK and VK together are what others need to pay you: SK to make you the owner of a
note, VK to encrypt that note so you can find it. Bundled with a chain tag and Bech32m
encoded, they become your meta-address — the hestia1…
string you share to receive funds.
