HESTIAdocs

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

ts
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
}
KeyKindWhat it does
sksecret scalarAuthorizes spends. Produces nullifiers. Must stay on the device.
SKpublic field elementA note's owner. Senders set owner = SK so only you can spend.
vkX25519 secretTrial-decrypts note ciphertexts to discover what you own. The unit of selective disclosure.
VKX25519 publicSenders 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:

text
sk = poseidon([toField(seed), 0])        SK = poseidon([sk])
vk = keccak256(seed ‖ 0x01)              VK = X25519(vk)
ts
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:

ts
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/v1 for 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.