diff --git a/docs/build/use-cases/key-generation.mdx b/docs/build/use-cases/key-generation.mdx
new file mode 100644
index 0000000000..73d0263b0d
--- /dev/null
+++ b/docs/build/use-cases/key-generation.mdx
@@ -0,0 +1,626 @@
+---
+description: Generate an Ethereum-compatible key inside ROFL via appd and use it to sign and send transactions on Base.
+tags: [ROFL, appd, KMS, EVM]
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# ROFL Key Generation (EVM / Base)
+
+This chapter shows how to build a tiny TypeScript app that **generates a
+secp256k1 key inside ROFL** via the [appd REST API], derives an **EVM
+address**, **signs** a message, **deploys a contract**, and **sends**
+EIP‑1559 transactions on **Base Sepolia**. We use a simple **smoke test**
+that prints to logs.
+
+[appd REST API]: ../rofl/features/appd.md
+
+## Prerequisites
+
+This guide requires:
+
+- **Node.js 20+** and **Docker** (or Podman).
+- **Oasis CLI** and at least **120 TEST** tokens in your wallet.
+- A Base Sepolia faucet or funds to test sending ETH.
+
+Check [Quickstart Prerequisites] for setup details.
+
+[Quickstart Prerequisites]: ../rofl/quickstart#prerequisites
+
+## Init App
+
+Initialize a new app using the [Oasis CLI]:
+
+```shell
+oasis rofl init rofl-keygen
+cd rofl-keygen
+```
+
+## Create App
+
+Create the app on Testnet (100 TEST deposit):
+
+```shell
+oasis rofl create --network testnet
+```
+
+The CLI prints the **App ID** (e.g., `rofl1...`).
+
+## Init a Hardhat (TypeScript) project
+
+```shell
+npx hardhat --init
+```
+
+When prompted, **choose TypeScript** and accept the defaults.
+
+Now add the small runtime deps we use outside of Hardhat:
+
+```shell
+npm i ethers dotenv
+npm i -D tsx
+```
+
+Using Hardhat’s TypeScript template, it already created a `tsconfig.json`.
+Add the following so our app code compiles to `dist/`:
+
+```json
+// tsconfig.json
+{
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./dist"
+ },
+ "include": ["src"]
+}
+```
+
+## App structure
+
+We'll add a few small TS files and one Solidity contract:
+
+```
+src/
+├── appd.ts # talks to appd over /run/rofl-appd.sock
+├── evm.ts # ethers helpers (provider, wallet, tx, deploy)
+├── keys.ts # tiny helpers (checksum)
+└── scripts/
+ ├── deploy-contract.ts # generic deploy script for compiled artifacts
+ └── smoke-test.ts # end-to-end demo (logs)
+contracts/
+└── Counter.sol # sample contract
+```
+
+### `src/appd.ts` — keygen over UNIX socket
+
+This calls `POST /rofl/v1/keys/generate` and returns a **0x‑hex secret
+key**. Outside ROFL, it can fall back to a **dev secret key** for local
+tests when explicitly allowed.
+
+
+ src/appd.ts
+
+```ts
+import { request } from "node:http";
+import { existsSync } from "node:fs";
+
+const APPD_SOCKET = "/run/rofl-appd.sock";
+type KeyKind = "secp256k1" | "ed25519" | "raw-256" | "raw-386";
+
+export async function generateKey(
+ keyId: string,
+ kind: KeyKind = "secp256k1"
+): Promise {
+ if (!existsSync(APPD_SOCKET)) {
+ throw new Error("appd socket missing: /run/rofl-appd.sock");
+ }
+ const body = JSON.stringify({ key_id: keyId, kind });
+ return new Promise((resolve, reject) => {
+ const req = request(
+ {
+ method: "POST",
+ socketPath: APPD_SOCKET,
+ path: "/rofl/v1/keys/generate",
+ headers: {
+ "Content-Type": "application/json",
+ "Content-Length": Buffer.byteLength(body).toString()
+ }
+ },
+ (res) => {
+ let data = "";
+ res.setEncoding("utf8");
+ res.on("data", (c) => (data += c));
+ res.on("end", () => {
+ try {
+ const { key } = JSON.parse(data);
+ if (!key) throw new Error(`Bad response: ${data}`);
+ resolve(key.startsWith("0x") ? key : `0x${key}`);
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
+ );
+ req.on("error", reject);
+ req.write(body);
+ req.end();
+ });
+}
+
+export async function getAppId(): Promise {
+ if (!existsSync(APPD_SOCKET)) {
+ throw new Error("appd socket missing: /run/rofl-appd.sock");
+ }
+ return new Promise((resolve, reject) => {
+ const req = request(
+ { method: "GET", socketPath: APPD_SOCKET, path: "/rofl/v1/app/id" },
+ (res) => {
+ let data = "";
+ res.setEncoding("utf8");
+ res.on("data", (c) => (data += c));
+ res.on("end", () => {
+ const id = data.trim();
+ if (!/^rofl1[0-9a-z]+$/.test(id)) {
+ return reject(new Error(`Bad app id: ${data}`));
+ }
+ resolve(id);
+ });
+ }
+ );
+ req.on("error", reject);
+ req.end();
+ });
+}
+
+export async function getEvmSecretKey(keyId: string): Promise {
+ try {
+ return await generateKey(keyId, "secp256k1");
+ } catch (e) {
+ const allow = process.env.ALLOW_LOCAL_DEV === "true";
+ const pk = process.env.LOCAL_DEV_SK;
+ if (allow && pk && /^0x[0-9a-fA-F]{64}$/.test(pk)) return pk;
+ throw e;
+ }
+}
+```
+
+
+
+### `src/evm.ts` — ethers helpers
+
+
+ src/evm.ts
+
+```ts
+import {
+ JsonRpcProvider,
+ Wallet,
+ parseEther,
+ type TransactionReceipt,
+ ContractFactory
+} from "ethers";
+
+export function makeProvider(rpcUrl: string, chainId: number) {
+ return new JsonRpcProvider(rpcUrl, chainId);
+}
+
+export function connectWallet(
+ skHex: string,
+ rpcUrl: string,
+ chainId: number
+): Wallet {
+ const w = new Wallet(skHex);
+ return w.connect(makeProvider(rpcUrl, chainId));
+}
+
+export async function signPersonalMessage(wallet: Wallet, msg: string) {
+ return wallet.signMessage(msg);
+}
+
+export async function sendEth(
+ wallet: Wallet,
+ to: string,
+ amountEth: string
+): Promise {
+ const tx = await wallet.sendTransaction({
+ to,
+ value: parseEther(amountEth)
+ });
+ const receipt = await tx.wait();
+ if (receipt == null) {
+ throw new Error("Transaction dropped or replaced before confirmation");
+ }
+ return receipt;
+}
+
+export async function deployContract(
+ wallet: Wallet,
+ abi: any[],
+ bytecode: string,
+ args: unknown[] = []
+): Promise<{ address: string; receipt: TransactionReceipt }> {
+ const factory = new ContractFactory(abi, bytecode, wallet);
+ const contract = await factory.deploy(...args);
+ const deployTx = contract.deploymentTransaction();
+ const receipt = await deployTx?.wait();
+ await contract.waitForDeployment();
+ if (!receipt) {
+ throw new Error("Deployment TX not mined");
+ }
+ return { address: contract.target as string, receipt };
+}
+```
+
+
+
+### `src/keys.ts` — tiny helpers
+
+
+ src/keys.ts
+
+```ts
+import { Wallet, getAddress } from "ethers";
+
+export function secretKeyToWallet(skHex: string): Wallet {
+ return new Wallet(skHex);
+}
+
+export function checksumAddress(addr: string): string {
+ return getAddress(addr);
+}
+```
+
+
+
+### `src/scripts/smoke-test.ts` — single end‑to‑end flow
+
+This script prints the App ID (inside ROFL), address, a signed message,
+waits for funding, and deploys the counter contract.
+
+
+ src/scripts/smoke-test.ts
+
+```ts
+import "dotenv/config";
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+import { getAppId, getEvmSecretKey } from "../appd.js";
+import { secretKeyToWallet, checksumAddress } from "../keys.js";
+import { makeProvider, signPersonalMessage, sendEth, deployContract } from "../evm.js";
+import { formatEther, JsonRpcProvider } from "ethers";
+
+const RPC_URL = process.env.BASE_RPC_URL ?? "https://sepolia.base.org";
+const CHAIN_ID = Number(process.env.BASE_CHAIN_ID ?? "84532");
+const KEY_ID = process.env.KEY_ID ?? "evm:base:sepolia";
+
+function sleep(ms: number): Promise {
+ return new Promise((r) => setTimeout(r, ms));
+}
+
+async function waitForFunding(
+ provider: JsonRpcProvider,
+ addr: string,
+ minWei: bigint = 1n,
+ timeoutMs = 15 * 60 * 1000,
+ pollMs = 5_000
+): Promise {
+ const start = Date.now();
+ while (Date.now() - start < timeoutMs) {
+ const bal = await provider.getBalance(addr);
+ if (bal >= minWei) return bal;
+ console.log(`Waiting for funding... current balance=${formatEther(bal)} ETH`);
+ await sleep(pollMs);
+ }
+ throw new Error("Timed out waiting for funding.");
+}
+
+async function main() {
+ const appId = await getAppId().catch(() => null);
+ console.log(`ROFL App ID: ${appId ?? "(unavailable outside ROFL)"}`);
+
+ const sk = await getEvmSecretKey(KEY_ID);
+ const wallet = secretKeyToWallet(sk).connect(makeProvider(RPC_URL, CHAIN_ID));
+ const addr = checksumAddress(await wallet.getAddress());
+ console.log(`EVM address (Base Sepolia): ${addr}`);
+
+ const msg = "hello from rofl";
+ const sig = await signPersonalMessage(wallet, msg);
+ console.log(`Signed message: "${msg}"`);
+ console.log(`Signature: ${sig}`);
+
+ const provider = wallet.provider as JsonRpcProvider;
+
+ let bal = await provider.getBalance(addr);
+ if (bal === 0n) {
+ console.log("Please fund the above address with Base Sepolia ETH to continue.");
+ bal = await waitForFunding(provider, addr);
+ }
+ console.log(`Balance detected: ${formatEther(bal)} ETH`);
+
+ const artifactPath = join(process.cwd(), "artifacts", "contracts", "Counter.sol", "Counter.json");
+ const artifact = JSON.parse(readFileSync(artifactPath, "utf8"));
+ if (!artifact?.abi || !artifact?.bytecode) {
+ throw new Error("Counter artifact missing abi/bytecode");
+ }
+ const { address: contractAddress, receipt: deployRcpt } =
+ await deployContract(wallet, artifact.abi, artifact.bytecode, []);
+ console.log(`Deployed Counter at ${contractAddress} (tx=${deployRcpt.hash})`);
+
+ console.log("Smoke test completed successfully!");
+}
+
+main().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
+```
+
+
+
+### `contracts/Counter.sol` — minimal sample
+
+
+ contracts/Counter.sol
+
+```solidity
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.24;
+
+contract Counter {
+ uint256 private _value;
+ event Incremented(uint256 v);
+ event Set(uint256 v);
+
+ function current() external view returns (uint256) { return _value; }
+ function inc() external { unchecked { _value += 1; } emit Incremented(_value); }
+ function set(uint256 v) external { _value = v; emit Set(v); }
+}
+```
+
+
+
+### `src/scripts/deploy-contract.ts` — generic deployer
+
+
+ src/scripts/deploy-contract.ts
+
+```ts
+import "dotenv/config";
+import { readFileSync } from "node:fs";
+import { getEvmSecretKey } from "../appd.js";
+import { secretKeyToWallet } from "../keys.js";
+import { makeProvider, deployContract } from "../evm.js";
+
+const KEY_ID = process.env.KEY_ID ?? "evm:base:sepolia";
+const RPC_URL = process.env.BASE_RPC_URL ?? "https://sepolia.base.org";
+const CHAIN_ID = Number(process.env.BASE_CHAIN_ID ?? "84532");
+
+/**
+ * Usage:
+ * npm run deploy-contract -- ./artifacts/MyContract.json '[arg0, arg1]'
+ * The artifact must contain { abi, bytecode }.
+ */
+async function main() {
+ const [artifactPath, ctorJson = "[]"] = process.argv.slice(2);
+ if (!artifactPath) {
+ console.error("Usage: npm run deploy-contract -- '[constructorArgsJson]'");
+ process.exit(2);
+ }
+
+ const artifactRaw = readFileSync(artifactPath, "utf8");
+ const artifact = JSON.parse(artifactRaw);
+ const { abi, bytecode } = artifact ?? {};
+ if (!abi || !bytecode) {
+ throw new Error("Artifact must contain { abi, bytecode }");
+ }
+
+ let args: unknown[];
+ try {
+ args = JSON.parse(ctorJson);
+ if (!Array.isArray(args)) throw new Error("constructor args must be a JSON array");
+ } catch (e) {
+ throw new Error(`Failed to parse constructor args JSON: ${String(e)}`);
+ }
+
+ const sk = await getEvmSecretKey(KEY_ID);
+ const wallet = secretKeyToWallet(sk).connect(makeProvider(RPC_URL, CHAIN_ID));
+ const { address, receipt } = await deployContract(wallet, abi, bytecode, args);
+
+ console.log(JSON.stringify({ contractAddress: address, txHash: receipt.hash, status: receipt.status }, null, 2));
+}
+
+main().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
+```
+
+
+
+## Hardhat (contracts only)
+
+Minimal config to compile `Counter.sol`:
+
+
+ hardhat.config.ts
+
+```ts
+import type { HardhatUserConfig } from "hardhat/config";
+
+const config: HardhatUserConfig = {
+ solidity: {
+ version: "0.8.24",
+ settings: {
+ optimizer: { enabled: true, runs: 200 }
+ }
+ },
+ paths: {
+ sources: "./contracts",
+ artifacts: "./artifacts",
+ cache: "./cache"
+ }
+};
+
+export default config;
+```
+
+
+
+Compile locally (optional):
+
+```shell
+npx hardhat compile
+```
+
+## Containerize
+
+Mount the **appd socket** provided by ROFL. No public ports are exposed.
+
+
+ compose.yaml
+
+```yaml
+services:
+ demo:
+ image: docker.io/YOURUSER/rofl-keygen:0.1.0
+ platform: linux/amd64
+ environment:
+ - KEY_ID=${KEY_ID:-evm:base:sepolia}
+ - BASE_RPC_URL=${BASE_RPC_URL:-https://sepolia.base.org}
+ - BASE_CHAIN_ID=${BASE_CHAIN_ID:-84532}
+ volumes:
+ - /run/rofl-appd.sock:/run/rofl-appd.sock
+```
+
+
+
+Add a Dockerfile that builds TS and compiles the contract, runs the
+**smoke test** once, then idles so you can inspect logs.
+
+
+ Dockerfile
+
+```dockerfile
+FROM node:20-alpine
+WORKDIR /app
+
+COPY package.json package-lock.json* ./
+RUN npm ci
+
+COPY tsconfig.json ./
+COPY src ./src
+COPY contracts ./contracts
+COPY hardhat.config.ts ./
+RUN npm run build && npx hardhat compile && npm prune --omit=dev
+
+ENV NODE_ENV=production
+CMD ["sh", "-c", "node dist/scripts/smoke-test.js || true; tail -f /dev/null"]
+```
+
+
+
+## Scripts
+
+Add handy scripts to `package.json`:
+
+```json
+{
+ "scripts": {
+ "build": "tsc -p tsconfig.json",
+ "build:all": "npm run build && npm run compile:contracts",
+ "smoke-test": "node dist/scripts/smoke-test.js",
+ "get-address": "tsx src/scripts/get-address.ts",
+ "sign-message": "tsx src/scripts/sign-message.ts",
+ "send-eth": "tsx src/scripts/send-eth.ts",
+ "get-app-id": "tsx src/scripts/get-app-id.ts",
+ "deploy-contract:dev": "tsx src/scripts/deploy-contract.ts",
+ "deploy-contract": "node dist/scripts/deploy-contract.js",
+ "compile:contracts": "hardhat compile",
+ "deploy-counter": "npm run compile:contracts && node dist/scripts/deploy-contract.js ./artifacts/contracts/Counter.sol/Counter.json \"[]\""
+ }
+}
+```
+
+## Build the image
+
+ROFL runs on **x86_64/TDX**, so build amd64 images:
+
+```shell
+docker buildx build --platform linux/amd64 \
+ -t docker.io/YOURUSER/rofl-keygen:0.1.0 --push .
+```
+
+(Optionally pin the digest and use `image: ...@sha256:...` in compose.)
+
+## Build ROFL bundle
+
+
+
+ ```shell
+ oasis rofl build
+ ```
+
+
+ ```shell
+ docker run --platform linux/amd64 --volume .:/src \
+ -it ghcr.io/oasisprotocol/rofl-dev:main oasis rofl build
+ ```
+
+
+
+Then publish the enclave identities and config:
+
+```shell
+oasis rofl update
+```
+
+## Deploy
+
+Deploy to a Testnet provider:
+
+```shell
+oasis rofl deploy
+```
+
+## End‑to‑end (Base Sepolia)
+
+1. **View smoke‑test logs**
+
+ ```shell
+ oasis rofl machine logs
+ ```
+
+ You should see:
+
+ * App ID
+ * EVM address and a signed message
+ * A prompt to fund the address
+ * After funding: a Counter.sol deployment
+
+2. **Local dev (optional)**
+
+ ```shell
+ export ALLOW_LOCAL_DEV=true
+ export LOCAL_DEV_SK=0x<64-hex-dev-secret-key> # DO NOT USE IN PROD
+ npm run get-address
+ npm run sign-message -- "hello from rofl"
+ npm run deploy-counter
+ npm run send-eth -- 0xYourSepoliaAddress 0.001
+ ```
+
+## Security & notes
+
+- **Never** log secret keys. Provider logs are not encrypted at rest.
+- The appd socket `/run/rofl-appd.sock` exists **only inside ROFL**.
+- Public RPCs may rate‑limit; prefer a dedicated Base RPC URL.
+
+That’s it! You generated a key in ROFL with **appd**, signed messages,
+deployed a contract, and moved ETH on Base Sepolia.
+
+:::example Key Generation Demo
+
+You can fetch a complete example shown in this chapter from
+https://github.com/oasisprotocol/demo-rofl-keygen.
+
+:::
diff --git a/sidebarBuild.ts b/sidebarBuild.ts
index 93b07751c5..eaf7cf0cb9 100644
--- a/sidebarBuild.ts
+++ b/sidebarBuild.ts
@@ -19,6 +19,7 @@ export const sidebarBuild: SidebarsConfig = {
items: [
'build/use-cases/price-oracle',
'build/use-cases/tgbot',
+ 'build/use-cases/key-generation',
]
},
{