{
 "version": 1,
 "site": "https://docs.zunialab.com",
 "title": "Zunia Docs",
 "summary": "How to connect a dApp to Zunia, a self-custody multi-chain Cosmos wallet (browser extension, phone app and web dashboard), and how to use the wallet itself.",
 "generatedAt": "2026-10-13T22:43:41.788Z",
 "commit": "3f6ad0c560da",
 "sections": [
  {
   "title": "Overview",
   "optional": false,
   "pages": [
    "intro",
    "faq"
   ]
  },
  {
   "title": "Get started",
   "optional": false,
   "pages": [
    "get-started/quickstart",
    "get-started/compatibility",
    "get-started/ai"
   ]
  },
  {
   "title": "Integrate",
   "optional": false,
   "pages": [
    "integrate/extension",
    "integrate/react",
    "integrate/sign-in",
    "integrate/events",
    "integrate/qr",
    "integrate/walletconnect",
    "integrate/suggest-chain",
    "integrate/flutter",
    "integrate/troubleshooting"
   ]
  },
  {
   "title": "Reference",
   "optional": false,
   "pages": [
    "reference/provider",
    "reference/sdk",
    "reference/events",
    "reference/errors",
    "reference/connect-v2",
    "reference/sign-in"
   ]
  },
  {
   "title": "Security",
   "optional": false,
   "pages": [
    "security/model"
   ]
  },
  {
   "title": "Use the wallet",
   "optional": false,
   "pages": [
    "use-wallet/extension",
    "use-wallet/mobile",
    "use-wallet/keys",
    "use-wallet/networks",
    "use-wallet/send-and-ibc",
    "use-wallet/staking",
    "use-wallet/fees"
   ]
  },
  {
   "title": "Chain registry",
   "optional": false,
   "pages": [
    "chain-registry/overview",
    "chain-registry/contribute"
   ]
  },
  {
   "title": "Brand",
   "optional": true,
   "pages": [
    "brand/assets"
   ]
  },
  {
   "title": "Legal (draft)",
   "optional": true,
   "pages": [
    "legal/terms",
    "legal/privacy",
    "legal/gdpr-data-map",
    "legal/app-store-encryption"
   ]
  },
  {
   "title": "Architecture",
   "optional": true,
   "pages": [
    "adr/overview"
   ]
  },
  {
   "title": "Developers",
   "optional": true,
   "pages": [
    "developers/reproducible-builds"
   ]
  }
 ],
 "pages": [
  {
   "id": "intro",
   "title": "How Zunia works",
   "description": "Zunia is a non-custodial Cosmos wallet. Keys stay on the device that created or imported them. A dApp never sees a seed phrase. It asks the wallet for addresses, signatures, and live account events.",
   "url": "https://docs.zunialab.com/docs/intro",
   "markdownUrl": "https://docs.zunialab.com/docs/intro.md",
   "section": "Overview",
   "optional": false,
   "headings": [
    "What you can do today",
    "What is not ready",
    "Next"
   ],
   "markdown": "# How Zunia works\n\nZunia is a non-custodial Cosmos wallet. Keys stay on the device that created or imported them. A dApp never sees a seed phrase. It asks the wallet for addresses, signatures, and live account events.\n\nThere are two products you can install, and three ways a site can reach a wallet:\n\n| Product | What it is | Status |\n|---------|------------|--------|\n| [Browser extension](https://github.com/Zunia-Lab/zunia-extension) | Chrome, Edge, Firefox and Safari (macOS and iOS) | On the [Chrome Web Store](https://chromewebstore.google.com/detail/zunia/ngokakoekdogobjmokipglbcclelgajk). Safari is in App Store review. Firefox is planned. Edge builds are not published. |\n| [Mobile wallet](https://github.com/Zunia-Lab/zunia-mobile) | iOS and Android | In App Store and Google Play review. WalletConnect sessions are accepted; WalletConnect signing is not implemented. |\n\nThe [web dashboard](https://app.zunialab.com) is not something you install, and it holds no keys. It connects the Zunia extension, Keplr, or Zunia mobile by QR code, and every transaction it prepares is signed in that wallet.\n\nA website talks to those products through the [JavaScript SDKs](https://docs.zunialab.com/docs/get-started/quickstart):\n\n1. **Extension.** The wallet injects `window.zunia`. This is the path to use in a desktop browser when Zunia is installed.\n2. **Phone, by QR code.** The site shows a code, Zunia mobile scans it, and both sides talk through a relay. Messages are encrypted end to end. Zunia runs the relay at `https://api.zunialab.com`, the one Zunia mobile trusts. Its source is [zunia-backend](https://github.com/Zunia-Lab/zunia-backend).\n3. **WalletConnect v2.** For other wallets. Do not use this to reach Zunia mobile until that app signs WalletConnect requests.\n\n```mermaid\nsequenceDiagram\n  participant Dapp as dApp SDK\n  participant Ext as Extension\n  participant Relay as Relay\n  participant Phone as Zunia mobile\n  Dapp->>Ext: enable, signIn, signAmino\n  Ext-->>Dapp: accounts and events\n  Dapp->>Relay: POST /v1/connect/sessions\n  Relay-->>Dapp: sessionId, dappToken, walletJoinToken\n  Dapp->>Dapp: QR with join token and public key\n  Phone->>Relay: WebSocket with join token\n  Note over Dapp,Phone: X25519, same 6-digit code, then encrypted frames\n  Phone-->>Dapp: accounts and signatures\n```\n\n## What you can do today\n\n- Create or import a wallet in the extension or the mobile app, on the same recovery phrase if you want both.\n- Connect a web dApp to the extension, restore the session after a reload, and hear account switches, locks and revocations live.\n- Sign in with no transaction: the wallet signs a message bound to your site, and your server checks it with `verifySignIn`.\n- Sign Amino and Direct transactions and broadcast them yourself (CosmJS or your own client).\n- Pair Zunia mobile with a site by QR code, through the Zunia relay.\n- Send, swap, stake and vote from the extension itself, using the WebAssembly kernel.\n- Use the web dashboard at [app.zunialab.com](https://app.zunialab.com) with the extension, Keplr or Zunia mobile.\n\n## What is not ready\n\n- Store listings other than the Chrome Web Store. Safari, the App Store and Google Play are in review. Firefox is planned. Edge has no listing.\n- A Flutter SDK for mobile dApps. The existing `zunia_sdk` package is deep links and a button. The v2 client is planned.\n- WalletConnect signing in Zunia mobile.\n- Hardware wallets (Ledger, Keystone).\n- An independent security audit. None has been completed.\n\n## Next\n\n- [Connect a site in five minutes](https://docs.zunialab.com/docs/get-started/quickstart)\n- [Browser and mobile status](https://docs.zunialab.com/docs/get-started/compatibility)\n- [Use the wallet](https://docs.zunialab.com/docs/use-wallet/extension)\n\nNeed help? Email [dev@zunialab.com](mailto:dev@zunialab.com) or [zunialab@gmail.com](mailto:zunialab@gmail.com). For vulnerabilities, write to dev@zunialab.com instead of opening a public issue.\n"
  },
  {
   "id": "faq",
   "title": "FAQ",
   "description": "The extension is on the Chrome Web Store. Safari, the App Store and Google Play are in review. Firefox is planned. For the others, load a build from GitHub. See Install the extension and Install the mobile wallet.",
   "url": "https://docs.zunialab.com/docs/faq",
   "markdownUrl": "https://docs.zunialab.com/docs/faq.md",
   "section": "Overview",
   "optional": false,
   "headings": [
    "Is Zunia in the stores?",
    "Can I use the same wallet on desktop and phone?",
    "How does a website connect?",
    "Can I sign in without sending a transaction?",
    "I scanned a WalletConnect code with Zunia mobile and nothing signs.",
    "Where is the relay?",
    "Are the SDKs on npm?",
    "Which chains work?",
    "Does Zunia charge fees?",
    "Is there a web dashboard I can paste my phrase into?",
    "Has Zunia been audited?",
    "Is it open source?",
    "How do I report a vulnerability?",
    "How do I get help?"
   ],
   "markdown": "# FAQ\n\n## Is Zunia in the stores?\n\nThe extension is on the [Chrome Web Store](https://chromewebstore.google.com/detail/zunia/ngokakoekdogobjmokipglbcclelgajk). Safari, the App Store and Google Play are in review. Firefox is planned. For the others, load a build from GitHub. See [Install the extension](https://docs.zunialab.com/docs/use-wallet/extension) and [Install the mobile wallet](https://docs.zunialab.com/docs/use-wallet/mobile).\n\n## Can I use the same wallet on desktop and phone?\n\nYes. Import the same recovery phrase on the second device. Each device holds its own encrypted copy. Zunia does not sync keys through a server. The two apps add further accounts differently; see [More accounts](https://docs.zunialab.com/docs/use-wallet/keys#more-accounts).\n\n## How does a website connect?\n\nThe JavaScript SDK talks to the extension (`window.zunia`), or to the phone through a QR code and a relay, or to other wallets through WalletConnect. Start at the [quickstart](https://docs.zunialab.com/docs/get-started/quickstart).\n\n## Can I sign in without sending a transaction?\n\nYes. [Sign in with Zunia](https://docs.zunialab.com/docs/integrate/sign-in) is an ADR-036 message bound to your domain. Your server checks it with `verifySignIn`.\n\n## I scanned a WalletConnect code with Zunia mobile and nothing signs.\n\nExpected. The app can open a WalletConnect session. It does not sign those requests yet. Use the Zunia QR (Connect with Zunia) instead.\n\n## Where is the relay?\n\nZunia runs it at `https://api.zunialab.com`. Its source is [zunia-backend](https://github.com/Zunia-Lab/zunia-backend), which implements `zunia.connect.v2`. See [Connect a phone by QR](https://docs.zunialab.com/docs/integrate/qr).\n\n## Are the SDKs on npm?\n\nYes. `@zunialab/sdk-web`, `@zunialab/sdk-react`, `@zunialab/sdk-core` and `@zunialab/interchain` are on npm.\n\n## Which chains work?\n\nThe bundled catalog is [zunia-chain-registry](https://github.com/Zunia-Lab/zunia-chain-registry). You can add a network by hand or a dApp can suggest one. Endpoints must be HTTPS.\n\n## Does Zunia charge fees?\n\nOnly on swaps made with Zunia's own **Swap** screen, in the browser extension or on the [web dashboard](https://app.zunialab.com). Such a swap carries a 0.5% commission, sent to Zunia. It is taken in the same transaction from the token you sell, and the review screen shows it before you sign. If the funds start on another chain and the swap runs on Osmosis, the commission is taken on that chain when the transfer is sent. If the swap then fails on Osmosis, the amount swapped comes back but the commission does not.\n\nA swap you make on another site or dApp and only sign with Zunia carries no Zunia commission. Sends, IBC transfers and staking carry no commission either. You pay the network fee shown before you sign, and Zunia takes no cut of staking rewards. See [Fees](https://docs.zunialab.com/docs/use-wallet/fees).\n\n## Is there a web dashboard I can paste my phrase into?\n\nNo, and there must never be. The web dashboard at [app.zunialab.com](https://app.zunialab.com) holds no keys and never asks for a phrase. It connects the Zunia extension, Keplr, or Zunia mobile by QR code, and every transaction is signed in that wallet. Signing keys stay in the extension or the phone.\n\n## Has Zunia been audited?\n\nNo. Do not claim otherwise.\n\n## Is it open source?\n\nYes. [github.com/Zunia-Lab](https://github.com/Zunia-Lab).\n\n## How do I report a vulnerability?\n\n[dev@zunialab.com](mailto:dev@zunialab.com), not a public issue.\n\n## How do I get help?\n\n[dev@zunialab.com](mailto:dev@zunialab.com), [zunialab@gmail.com](mailto:zunialab@gmail.com), or a GitHub issue on the relevant repo.\n"
  },
  {
   "id": "get-started/quickstart",
   "title": "Quickstart",
   "description": "This page gets a React app talking to the Zunia extension: connect, restore after a reload, and sign in. The same session object signs transactions with CosmJS.",
   "url": "https://docs.zunialab.com/docs/get-started/quickstart",
   "markdownUrl": "https://docs.zunialab.com/docs/get-started/quickstart.md",
   "section": "Get started",
   "optional": false,
   "headings": [
    "1. Connect",
    "2. Sign in (no transaction)",
    "3. Send with CosmJS",
    "Example",
    "Next"
   ],
   "markdown": "# Connect a site in five minutes\n\nThis page gets a React app talking to the Zunia extension: connect, restore after a reload, and sign in. The same session object signs transactions with CosmJS.\n\nInstall from npm:\n\n```bash\npnpm add @zunialab/sdk-react @zunialab/sdk-web\n```\n\nA server that checks sign-in uses `@zunialab/sdk-core`. IBC routes and packet memos are `@zunialab/interchain`.\n\nYou also need the Zunia extension. Install it from the [Chrome Web Store](https://chromewebstore.google.com/detail/zunia/ngokakoekdogobjmokipglbcclelgajk), or [build it](https://docs.zunialab.com/docs/use-wallet/extension) for another browser. Create a wallet there before you click Connect.\n\n## 1. Connect\n\n```tsx\nimport { ConnectWithZuniaButton, useZuniaSession } from \"@zunialab/sdk-react\";\n\nconst options = { chains: [\"cosmoshub-4\"] };\n\nexport function Connect() {\n  const zunia = useZuniaSession({ restore: options });\n\n  if (zunia.restoring) return null;\n  if (zunia.locked) {\n    return <button onClick={() => zunia.unlock().catch(() => {})}>Unlock Zunia</button>;\n  }\n  if (zunia.connected) {\n    return <p>Connected as {zunia.accounts[0]?.address}</p>;\n  }\n  return (\n    <ConnectWithZuniaButton\n      loading={zunia.connecting}\n      onClick={() => zunia.connect(options).catch(() => {})}\n    />\n  );\n}\n```\n\n`useZuniaSession({ restore: options })` reattaches after a reload without opening a window. `connect` asks the extension for those chains. The user approves in the toolbar popup (Chrome also draws an in-page prompt when it can tell the page is visible).\n\nIf the wallet was locked when the page loaded, `zunia.locked` is true and `accounts` stays empty. `unlock()` opens the unlock window and keeps your site's grant. Call it from a click.\n\n## 2. Sign in (no transaction)\n\nYour server issues a nonce. The wallet signs a message that names your domain. You check the result on the server.\n\n```ts\n// Browser\nconst { nonce } = await (await fetch(\"/api/nonce\", { method: \"POST\" })).json();\nconst proof = await zunia.signIn({ nonce });\nawait fetch(\"/api/verify\", { method: \"POST\", body: JSON.stringify({ nonce, ...proof }) });\n```\n\n```ts\n// Server\nimport { createNonce, verifySignIn } from \"@zunialab/sdk-core\";\n\nconst nonce = createNonce();\n// store nonce for 10 minutes, then consume it once\n\nconst { address } = verifySignIn({\n  message,\n  signature,\n  nonce,\n  domain: \"app.example.com\", // from your config, not from the request Host\n});\n```\n\nThe wallet refuses a message whose domain is not the site asking. See [Sign in with Zunia](https://docs.zunialab.com/docs/integrate/sign-in).\n\n## 3. Send with CosmJS\n\n```ts\nimport { SigningStargateClient } from \"@cosmjs/stargate\";\n\nconst [account] = zunia.accounts;\nconst messages = [\n  {\n    typeUrl: \"/cosmos.bank.v1beta1.MsgSend\",\n    value: { fromAddress: account.address, toAddress: recipient, amount: [{ denom: \"uatom\", amount: \"1000\" }] },\n  },\n];\nconst signer = zunia.session.getOfflineSignerFor(\"cosmoshub-4\", { messages, memo });\nconst client = await SigningStargateClient.connectWithSigner(rpcUrl, signer);\nawait client.signAndBroadcast(account.address, messages, \"auto\", memo);\n```\n\n`getOfflineSignerFor` hands CosmJS the signer the connected Zunia build can sign these messages with, so contract calls also work on older extension builds. Results are `Uint8Array` and `bigint` on every transport.\n\n## Example\n\n[`examples/dapp`](https://github.com/Zunia-Lab/zunia-sdk/tree/main/examples/dapp) in zunia-sdk is a Vite app with connect, verified sign-in, a CosmJS send and a live event log. It is what the end-to-end tests drive.\n\n## Next\n\n- [React guide](https://docs.zunialab.com/docs/integrate/react)\n- [Extension without React](https://docs.zunialab.com/docs/integrate/extension)\n- [Pair a phone by QR](https://docs.zunialab.com/docs/integrate/qr)\n- [Events and session lifecycle](https://docs.zunialab.com/docs/integrate/events)\n"
  },
  {
   "id": "get-started/compatibility",
   "title": "Compatibility",
   "description": "Honest status for each surface. The extension is on the Chrome Web Store. Safari, the App Store and Google Play are in review. The JavaScript packages are on npm.",
   "url": "https://docs.zunialab.com/docs/get-started/compatibility",
   "markdownUrl": "https://docs.zunialab.com/docs/get-started/compatibility.md",
   "section": "Get started",
   "optional": false,
   "headings": [
    "Browsers",
    "Mobile",
    "SDKs",
    "Relay",
    "Signing",
    "Chains"
   ],
   "markdown": "# Compatibility and status\n\nHonest status for each surface. The extension is on the Chrome Web Store. Safari, the App Store and Google Play are in review. The JavaScript packages are on npm.\n\n## Browsers\n\nThe extension is one Manifest V3 codebase. Each browser gets its own build. Details and the test matrix live in [zunia-extension/docs/browsers.md](https://github.com/Zunia-Lab/zunia-extension/blob/main/docs/browsers.md).\n\n| | Chrome, Brave, Opera | Edge | Firefox | Safari |\n| --- | --- | --- | --- | --- |\n| Build | `.output/chrome-mv3` | `.output/edge-mv3` | `.output/firefox-mv3` | `.output/safari-mv3` plus the Xcode app |\n| Connect, sign-in, signature, live account switch and revoke | Automated ([zunia-e2e](https://github.com/Zunia-Lab/zunia-e2e)) | Same Chromium build | Automated | By hand on the iOS Simulator |\n| In-page connect prompt | Yes, when the page is visible | Yes | No. Requests wait in the toolbar popup. | No. Toolbar popup, or a tab on iOS when the popup cannot open. |\n| Published | [Chrome Web Store](https://chromewebstore.google.com/detail/zunia/ngokakoekdogobjmokipglbcclelgajk) | No | No, planned | In App Store review |\n\nThe Firefox build needs Firefox 140 or later. The Safari app needs iOS 17 or macOS 13 and later. Firefox unloads an idle background page after about 30 seconds. The unlocked session lives in `storage.session` and survives that. Safari on iOS stops an idle worker after about 8 seconds; a page that uses the wallet, and the approval screen, ping the worker so requests still complete. Safari also drops `tabs.sendMessage`, so dApp events travel over a port the content script opens. See [the provider events](https://docs.zunialab.com/docs/reference/events#safari).\n\nmacOS Safari has not been run end to end. It needs **Allow unsigned extensions** in Safari Developer Settings, which Safari turns off when it quits.\n\n## Mobile\n\n| | iOS | Android |\n| --- | --- | --- |\n| App | Flutter, iOS 13+ in the current project | Flutter, Android 7.0+ (API 24) in the current project |\n| QR pairing (`zunia.connect.v2`) | Implemented | Implemented |\n| WalletConnect session | Accepted when a Cloud project id is set | Same |\n| WalletConnect signing | Not implemented | Not implemented |\n| Store | In App Store review | In Google Play review |\n\nPair Zunia mobile with a site by QR, not by WalletConnect, until signing exists.\n\n## SDKs\n\n| Package | Install | Status |\n|---------|---------|--------|\n| `@zunialab/sdk-web` | Browser dApps | On npm |\n| `@zunialab/sdk-react` | React 18 or 19 | On npm |\n| `@zunialab/sdk-core` | Servers and wallets | On npm |\n| `@zunialab/interchain` | IBC routes and memos | On npm |\n| `zunia_sdk` (Dart) | Flutter dApps | Deep links and a button. The v2 session client is planned. |\n\n## Relay\n\n[zunia-backend](https://github.com/Zunia-Lab/zunia-backend) implements `zunia.connect.v2`. Zunia runs it at `https://api.zunialab.com` (WebSocket `wss://api.zunialab.com/v1/connect/ws`). Any site may open sessions there, within per-address rate limits. Zunia mobile only pairs through relays on its trusted list, which holds `wss://api.zunialab.com` by default, so pass `apiBase: \"https://api.zunialab.com\"`.\n\nTo test against a relay of your own, `pnpm dev` in zunia-backend listens on `http://localhost:8788`. Only a debug build of Zunia mobile, or one built with that relay on its trusted list, pairs through it.\n\n## Signing\n\n- Cosmos Amino and Direct: supported in the extension and over QR pairing.\n- Zunia extension 0.1.4 and older refuse, in Direct mode, a contract call to a 32-byte contract or a send to a 32-byte address. They also sign Amino documents holding `&`, `<` or `>` over bytes the chain rejects. 0.1.5 fixes both and reports `extensionVersion` and `features` on `window.zunia`. `session.getOfflineSignerFor()` picks the mode each build can sign.\n- Sign in with Zunia: ADR-036 message bound to the site. Supported.\n- `sendTx` on `window.zunia`: not supported. The dApp broadcasts.\n- Ethermint / coin type 60: the wallet derives those addresses. It does not expose an Ethereum provider (`eth_*`, EIP-712). Do not treat it as MetaMask.\n- Hardware wallets: not supported.\n\n## Chains\n\nThe bundled catalog comes from [zunia-chain-registry](https://github.com/Zunia-Lab/zunia-chain-registry). A site can [suggest a chain](https://docs.zunialab.com/docs/integrate/suggest-chain). A user can add one under **Manage networks**. Endpoints must be `https://`.\n"
  },
  {
   "id": "get-started/ai",
   "title": "Build with AI",
   "description": "Connect Claude, Cursor, VS Code or any MCP client to the Zunia docs and SDK, and the llms.txt files for everything else.",
   "url": "https://docs.zunialab.com/docs/get-started/ai",
   "markdownUrl": "https://docs.zunialab.com/docs/get-started/ai.md",
   "section": "Get started",
   "optional": false,
   "headings": [
    "MCP server",
    "Claude Code",
    "Claude Desktop and claude.ai",
    "Cursor",
    "VS Code",
    "Clients that start a local server",
    "What it does",
    "llms.txt",
    "Keep keys out of prompts"
   ],
   "markdown": "# Build with AI\n\nCoding assistants can work from these docs in two ways: the Zunia MCP server, or plain-text files made for language models. Both are built from the docs each time the docs are published, so they say what this site says.\n\n## MCP server\n\n- **Hosted endpoint:** `https://docs.zunialab.com/mcp` (Streamable HTTP, no key).\n- **npm package:** `@zunialab/mcp`, run over stdio with `npx -y @zunialab/mcp`.\n\nThe hosted endpoint needs no install. Use the npm package when your client only starts local servers.\n\n### Claude Code\n\n```bash\nclaude mcp add --transport http zunia https://docs.zunialab.com/mcp\n```\n\n### Claude Desktop and claude.ai\n\nAdd a custom connector with the URL `https://docs.zunialab.com/mcp`.\n\n### Cursor\n\nIn `.cursor/mcp.json` (this project) or `~/.cursor/mcp.json` (every project):\n\n```json\n{\n  \"mcpServers\": {\n    \"zunia\": { \"url\": \"https://docs.zunialab.com/mcp\" }\n  }\n}\n```\n\n### VS Code\n\nIn `.vscode/mcp.json`:\n\n```json\n{\n  \"servers\": {\n    \"zunia\": { \"type\": \"http\", \"url\": \"https://docs.zunialab.com/mcp\" }\n  }\n}\n```\n\n### Clients that start a local server\n\n```json\n{\n  \"mcpServers\": {\n    \"zunia\": { \"command\": \"npx\", \"args\": [\"-y\", \"@zunialab/mcp\"] }\n  }\n}\n```\n\n### What it does\n\n| Tool | What it does |\n|------|--------------|\n| `search_docs` | Searches these docs and returns the best sections with their links |\n| `get_doc`, `list_docs` | Reads a page, or one section of it, and lists every page |\n| `list_integrations`, `get_integration_guide` | The integration paths, each with its packages at their latest npm version and the install command |\n| `get_sdk_packages` | The `@zunialab` packages, read from npm when you ask |\n| `get_chain`, `check_address` | A chain's entry in the [Zunia chain registry](https://docs.zunialab.com/docs/chain-registry/overview), and whether an address belongs to a chain |\n| `explain_error` | What an error code means and what to do next, from the SDK's `explainZuniaError` |\n| `verify_sign_in` | Runs `verifySignIn` on a sign-in you captured and says which check fails |\n| `ibc_denom`, `build_forward_memo` | Computes an IBC denom and builds a packet-forward memo with `@zunialab/interchain` |\n\nEach docs page is also offered as a resource, and the `integrate_zunia` prompt plans an integration for the project you have open.\n\nThe server is read-only. It never signs, holds no keys and needs no account.\n\n## llms.txt\n\n| File | What it holds |\n|------|---------------|\n| [llms.txt](https://docs.zunialab.com/llms.txt) | Every page, with a one-line description |\n| [llms-full.txt](https://docs.zunialab.com/llms-full.txt) | Every page in one file |\n| `/docs/…/page.md` | Any page as Markdown: add `.md` to its address, for example [quickstart.md](https://docs.zunialab.com/docs/get-started/quickstart.md) |\n| [docs-index.json](https://docs.zunialab.com/docs-index.json) | The same pages as JSON. The MCP server reads this file |\n\nStore availability and other product facts are in the website's own [llms.txt](https://zunialab.com/llms.txt).\n\n## Keep keys out of prompts\n\nNever paste a recovery phrase or a private key into an assistant, a prompt or an MCP client. Integrating Zunia never needs one, and the wallet never shows its keys to a site.\n"
  },
  {
   "id": "integrate/extension",
   "title": "Web dApp (extension)",
   "description": "Use @zunialab/sdk-web when you are not on React, or when you want the session object yourself. The extension injects window.zunia on https:// pages and on localhost.",
   "url": "https://docs.zunialab.com/docs/integrate/extension",
   "markdownUrl": "https://docs.zunialab.com/docs/integrate/extension.md",
   "section": "Integrate",
   "optional": false,
   "headings": [
    "Detect the extension",
    "Approve, then use",
    "Sign and broadcast",
    "Raw `window.zunia`"
   ],
   "markdown": "# Connect from a web dApp\n\nUse `@zunialab/sdk-web` when you are not on React, or when you want the session object yourself. The extension injects `window.zunia` on `https://` pages and on localhost.\n\n```ts\nimport { createZuniaSession } from \"@zunialab/sdk-web\";\n\nconst options = {\n  chains: [\"cosmoshub-4\", \"osmosis-1\"],\n  metadata: { name: \"My dApp\", url: location.origin },\n};\n\nconst session = createZuniaSession();\n\nif (!(await session.restore(options))) {\n  await session.connect({ ...options, prefer: \"extension\" });\n}\n\nconst [account] = session.accounts;\n```\n\n`restore` does not prompt. It calls `getConnectedChains()` on the provider and, if your site is still granted, reattaches. If the wallet is locked, status becomes `locked`, `accounts` stays empty, and later requests wait for unlock. `session.unlock()`, called from a click, opens the unlock window and keeps your grant. A CosmJS signer from the session asks for the unlock by itself when the user signs.\n\n## Detect the extension\n\n```ts\nimport { getZunia, isZuniaInstalled } from \"@zunialab/sdk-web\";\n\nisZuniaInstalled(); // synchronous, current window only\nconst provider = await getZunia(); // waits for `zunia#initialized`, or undefined\n```\n\n`getZunia({ preferAlias: true })` also accepts `window.keplr`, but only when that object says it is Zunia (`isZunia`, from extension 0.1.5). Another wallet's `window.keplr` is never taken for Zunia. Prefer `window.zunia`.\n\nWith the default `prefer: \"auto\"`, when nothing can connect, `connect` opens the install page unless you pass `openInstallIfMissing: false`, and throws `NOT_INSTALLED`. With `prefer: \"extension\"` it throws `NOT_INSTALLED` without opening anything.\n\n## Approve, then use\n\n`enable` / `connect` is the only call that should open a window for a first visit. After that:\n\n| Call | Window? |\n|------|---------|\n| `getConnectedChains()` | Never |\n| `getKey`, `getAccounts` | Only to unlock a locked wallet. Without a grant they fail with `NOT_CONNECTED`. |\n| `signIn`, `signAmino`, `signDirect`, `signArbitrary` | Always, so the user can read the request |\n| `experimentalSuggestChain` | Only when the chain is unknown |\n\nOn Chrome the first connect can appear as an in-page card. Firefox and Safari have no way to know the card is visible, so they answer from the toolbar icon. On Safari iOS, if the popup cannot open, the same screen opens in a tab and closes itself after the last answer.\n\n## Sign and broadcast\n\n```ts\nimport { SigningStargateClient } from \"@cosmjs/stargate\";\n\nconst messages = [\n  {\n    typeUrl: \"/cosmos.bank.v1beta1.MsgSend\",\n    value: { fromAddress: account.address, toAddress: recipient, amount: [{ denom: \"uatom\", amount: \"1000\" }] },\n  },\n];\nconst signer = session.getOfflineSignerFor(\"cosmoshub-4\", { messages, memo });\nconst client = await SigningStargateClient.connectWithSigner(rpc, signer);\nawait client.signAndBroadcast(account.address, messages, \"auto\", memo);\n```\n\nCosmJS signs Direct whenever its signer can. `getOfflineSignerFor` hands it the signer the connected Zunia build can sign these messages with. Zunia 0.1.4 and older refuse a Direct contract call to a 32-byte contract, or a send to a 32-byte address, so for those it returns the Amino-only signer. From 0.1.5, and on other transports, it returns the full signer. `session.capabilities` says what the connected extension can sign (`null` on other transports).\n\nOn those older builds, an Amino signature over `&`, `<` or `>` that the chain would refuse is stopped before anything is broadcast: `UNSUPPORTED` with `reason: \"amino-escaping\"`.\n\n`getOfflineSigner` (Direct and Amino), `getOfflineSignerOnlyAmino`, `signAmino`, `signDirect` and `signArbitrary` are also on the session. The extension never broadcasts a dApp transaction (`sendTx` is `UNSUPPORTED`).\n\n## Raw `window.zunia`\n\nYou can call the provider without the SDK. See [the provider API](https://docs.zunialab.com/docs/reference/provider). The SDK is the path that restores sessions, normalizes results and listens for events the same way on every transport.\n"
  },
  {
   "id": "integrate/react",
   "title": "React",
   "description": "@zunialab/sdk-react is a thin layer on @zunialab/sdk-web. Components are client components (\"use client\"), so they work in the Next.js App Router.",
   "url": "https://docs.zunialab.com/docs/integrate/react",
   "markdownUrl": "https://docs.zunialab.com/docs/integrate/react.md",
   "section": "Integrate",
   "optional": false,
   "headings": [
    "One hook",
    "Sign in",
    "Components"
   ],
   "markdown": "# React\n\n`@zunialab/sdk-react` is a thin layer on `@zunialab/sdk-web`. Components are client components (`\"use client\"`), so they work in the Next.js App Router.\n\n```bash\npnpm add @zunialab/sdk-react @zunialab/sdk-web\n```\n\n## One hook\n\n```tsx\nimport { ConnectPairingModal, ConnectWithZuniaButton, useZuniaSession } from \"@zunialab/sdk-react\";\n\nconst options = {\n  chains: [\"cosmoshub-4\"],\n  apiBase: \"https://api.zunialab.com\", // QR pairing with Zunia mobile; leave out to offer the extension only\n};\n\nexport function Wallet() {\n  const zunia = useZuniaSession({ restore: options });\n\n  if (zunia.restoring) return null;\n  if (zunia.connected) {\n    return (\n      <>\n        <p>{zunia.accounts[0]?.address}</p>\n        {zunia.locked && <button onClick={() => zunia.unlock().catch(() => {})}>Unlock Zunia</button>}\n        <button onClick={() => zunia.disconnect()}>Disconnect</button>\n      </>\n    );\n  }\n\n  return (\n    <>\n      <ConnectWithZuniaButton\n        loading={zunia.connecting}\n        onClick={() => zunia.connect(options).catch(() => {})}\n      />\n      <ConnectPairingModal\n        open={zunia.status === \"awaiting_wallet\"}\n        onOpenChange={(open) => !open && zunia.disconnect()}\n        status={zunia.status}\n        pairing={zunia.pairing}\n        verificationCode={zunia.verificationCode}\n        error={zunia.error}\n      />\n    </>\n  );\n}\n```\n\nThe hook re-renders when the wallet switches accounts, locks, unlocks or revokes your site.\n\n| Field | |\n|-------|--|\n| `status`, `transport` | See [events](https://docs.zunialab.com/docs/integrate/events). |\n| `accounts`, `chains` | What your site may use right now. |\n| `pairing`, `verificationCode` | For the QR dialog. |\n| `error` | Last `ZuniaConnectError`. `explainZuniaError(error)` puts it in words for your UI. |\n| `connected`, `locked`, `connecting`, `restoring` | Shortcuts. `connected` stays true while `locked`. After a restore that found the wallet locked, `accounts` is empty until the user unlocks. |\n| `connect`, `disconnect`, `restore`, `unlock`, `signIn` | Actions. `unlock` opens the wallet's unlock window and keeps your site's grant: call it from a click. |\n| `session` | The underlying session (`getOfflineSignerFor`, `on`, …). |\n\nCall the hook once near the root, or create `const session = createZuniaSession()` at module scope and pass `{ session }` to every caller.\n\n## Sign in\n\n```tsx\nconst onSignIn = async () => {\n  const { nonce } = await (await fetch(\"/api/nonce\", { method: \"POST\" })).json();\n  const proof = await zunia.signIn({ nonce });\n  await fetch(\"/api/verify\", { method: \"POST\", body: JSON.stringify({ nonce, ...proof }) });\n};\n```\n\nVerify on the server with `verifySignIn` from `@zunialab/sdk-core`.\n\n## Components\n\n- `ConnectWithZuniaButton`: official button. Opens the install page when `installed` is false and there is no `onClick`.\n- `ConnectPairingModal`: QR code, then the 6-digit check. On a phone it also offers a link that opens the Zunia app. Cancel should call `disconnect()`.\n- `ZuniaQrCode`: SVG, rendered locally.\n- `ZuniaMark`: the logo.\n- `useZunia()`: detects `window.zunia` only.\n"
  },
  {
   "id": "integrate/sign-in",
   "title": "Sign in with Zunia",
   "description": "Use this when you only need to know which Cosmos account is at the keyboard. There is no transaction and no fee.",
   "url": "https://docs.zunialab.com/docs/integrate/sign-in",
   "markdownUrl": "https://docs.zunialab.com/docs/integrate/sign-in.md",
   "section": "Integrate",
   "optional": false,
   "headings": [
    "Browser",
    "Server",
    "What the user signs",
    "Over QR pairing"
   ],
   "markdown": "# Sign in with Zunia\n\nUse this when you only need to know which Cosmos account is at the keyboard. There is no transaction and no fee.\n\nThe wallet shows a dedicated **Sign in to &lt;site&gt;** screen: your domain, the account and the expiry. A generic `signArbitrary` of the same bytes is refused if the domain does not match the caller.\n\n## Browser\n\n```ts\nconst { nonce } = await (await fetch(\"/api/nonce\", { method: \"POST\" })).json();\nconst { message, signature, address } = await session.signIn({\n  nonce,\n  statement: \"Sign in to My dApp.\", // optional\n});\nawait fetch(\"/api/verify\", { method: \"POST\", body: JSON.stringify({ nonce, message, signature }) });\n```\n\n`session.signIn()` fills `location.host` as the domain and `location.origin` as the URI. The message expires after 10 minutes unless you pass `expirationTime`.\n\n## Server\n\n```ts\nimport { createNonce, verifySignIn, ZuniaSignInError } from \"@zunialab/sdk-core\";\n\n// POST /api/nonce\nconst nonce = createNonce();\nawait nonces.save(nonce, { ttlSeconds: 600 });\n\n// POST /api/verify  { nonce, message, signature }\nif (!(await nonces.consume(nonce))) throw new Error(\"Unknown or used nonce\");\ntry {\n  const { address, chainId } = verifySignIn({\n    message,\n    signature,\n    nonce,\n    domain: \"app.example.com\",\n    chainId: [\"cosmoshub-4\"], // optional\n  });\n} catch (error) {\n  if (error instanceof ZuniaSignInError) console.warn(error.code);\n  throw error;\n}\n```\n\n`domain` must come from your config, not from the request `Host` header.\n\n`verifySignIn` checks, in order: the message format, the domain, that the URI is on that domain, the nonce, the chain, the address if you pass one, the dates (issued less than 10 minutes ago by default, not expired, not before `notBefore`), that the key is a compressed secp256k1 key, that it derives the signed address, and the ADR-036 signature.\n\nStoring each nonce and consuming it once is your job. That is what stops a captured signature from being replayed.\n\n## What the user signs\n\n```text\napp.example.com wants you to sign in with your Cosmos account:\ncosmos1fr389pmrhma7sqmmshzpvg6hyn9jzegzvx6y6x\n\nSign in to Example.\n\nURI: https://app.example.com\nVersion: 1\nChain ID: cosmoshub-4\nNonce: 8f1c2a9d4b7e6f30a5c1d2e3f4a5b6c7\nIssued At: 2026-09-24T10:00:00.000Z\nExpiration Time: 2026-09-24T10:10:00.000Z\n```\n\nThe wallet signs it as ADR-036 arbitrary data, so it can never be a transaction. Field rules are in [the sign-in reference](https://docs.zunialab.com/docs/reference/sign-in).\n\n## Over QR pairing\n\nThe phone binds the message to the **verified origin** the relay recorded from the page's `Origin` header, not to whatever URL the dApp put in its metadata. If those differ, the wallet warns and refuses sign-in. See [QR connection](https://docs.zunialab.com/docs/integrate/qr#verified-origin).\n"
  },
  {
   "id": "integrate/events",
   "title": "Events and sessions",
   "description": "A connected site should treat the wallet as the source of truth. The user can switch accounts, lock, add a chain or revoke you without touching your UI.",
   "url": "https://docs.zunialab.com/docs/integrate/events",
   "markdownUrl": "https://docs.zunialab.com/docs/integrate/events.md",
   "section": "Integrate",
   "optional": false,
   "headings": [
    "Status",
    "Wallet events",
    "What is stored",
    "Timeouts and drops"
   ],
   "markdown": "# Events and session lifecycle\n\nA connected site should treat the wallet as the source of truth. The user can switch accounts, lock, add a chain or revoke you without touching your UI.\n\n## Status\n\n| Status | Meaning |\n|--------|---------|\n| `idle` | Nothing started. |\n| `connecting` | Looking for a wallet, or waiting for the user to approve in the extension. |\n| `awaiting_wallet` | A QR code or WalletConnect link is waiting for the wallet to scan and approve it. |\n| `connected` | You have accounts. |\n| `locked` | Still granted, the wallet is locked. Requests wait for unlock. `session.unlock()`, called from a click, opens the unlock window. |\n| `reconnecting` | The relay socket dropped and is coming back. |\n| `disconnected` | You called `disconnect()`, the user revoked you, the session expired, or a connect failed (see `error`). |\n| `error` | Part of the type, but the SDK does not set it. A failed connect ends in `disconnected`, with the failure in `error`. |\n\n```ts\nsession.on(\"status\", (status) => setStatus(status));\nsession.on(\"accountsChanged\", (accounts) => render(accounts));\nsession.on(\"chainChanged\", (chains) => setChains(chains));\nsession.on(\"disconnect\", (reason) => showSignedOut(reason));\n```\n\nOn React, `useZuniaSession` already subscribes. `session.subscribe` / `session.getSnapshot()` work with `useSyncExternalStore`.\n\n## Wallet events\n\nThese are what `window.zunia` emits, and what the SDK forwards.\n\n| Event | When | Payload |\n|-------|------|---------|\n| `accountsChanged` | Active account changed or was renamed, or the wallet unlocked. Connected sites only. | None on `window.zunia`: read `getKey` again. The SDK does that and emits the new accounts. |\n| `chainChanged` | Chains granted to your origin changed. | Remaining chain ids |\n| `disconnect` | Access revoked or expired. | `null` for every chain, or `{ chainIds }` for those lost |\n| `locked` | The wallet locked. | none |\n\nA grant in the extension lasts 7 days. When it runs out, your site gets `disconnect` and has to connect again.\n\nAfter a reload, call `restore` (or `getConnectedChains()`). That does not unlock the wallet and does not open a window.\n\n## What is stored\n\nBy default the SDK writes to `localStorage`:\n\n- `zunia.session.transport`\n- `zunia.connect.v2.session` (relay URL, session id, dApp token, session keys and counters; never the QR join token)\n- `zunia.walletconnect.session`\n\nAny script on your origin can read these. Keep a strict Content Security Policy. Pass `storage: null` to keep the session in memory only (users pair again after a reload), or pass your own `{ getItem, setItem, removeItem }`.\n\n## Timeouts and drops\n\nOver QR pairing and WalletConnect, each connect and each signature waits 5 minutes by default (`timeoutMs`, `requestTimeoutMs`). In the extension, a prompt nobody answers expires after 5 minutes, and the SDK rejects with `TIMEOUT` (`reason: \"prompt-expired\"`). Over QR pairing the SDK pings every 20 seconds, reconnects with backoff, and resends requests the relay had not confirmed. If the relay forgot the session, you get `SESSION_EXPIRED` and the user pairs again.\n\n`disconnect()` from your page ends the session. On the extension it also revokes your site's grant for the session's chains (`window.zunia.disable`), so the next `connect` asks the user again. Over QR pairing it closes the relay session and deletes the stored one. The user can also revoke your site under **Connected dApps**.\n"
  },
  {
   "id": "integrate/qr",
   "title": "Connect a phone by QR",
   "description": "A page and Zunia mobile pair through zunia.connect.v2. The relay only forwards ciphertext. Both screens show the same 6-digit code so the user can see they are talking to each other.",
   "url": "https://docs.zunialab.com/docs/integrate/qr",
   "markdownUrl": "https://docs.zunialab.com/docs/integrate/qr.md",
   "section": "Integrate",
   "optional": false,
   "headings": [
    "What travels",
    "Verified origin",
    "After a reload",
    "Deep links"
   ],
   "markdown": "# Connect a phone by QR\n\nA page and Zunia mobile pair through `zunia.connect.v2`. The relay only forwards ciphertext. Both screens show the same 6-digit code so the user can see they are talking to each other.\n\nZunia runs the relay at `https://api.zunialab.com`. Pass it as `apiBase`. Any site may open sessions there, within per-address rate limits. Its source is [zunia-backend](https://github.com/Zunia-Lab/zunia-backend).\n\n```ts\nimport { createZuniaSession, renderQrSvg } from \"@zunialab/sdk-web\";\n\nconst session = createZuniaSession();\n\nsession.on(\"pairing\", ({ uri }) => {\n  dialog.innerHTML = renderQrSvg(uri, { size: 240, label: \"Scan with Zunia\" });\n});\nsession.on(\"verification\", (code) => {\n  dialog.textContent = `Your phone should show ${code.slice(0, 3)} ${code.slice(3)}`;\n});\n\nawait session.connect({\n  chains: [\"cosmoshub-4\"],\n  prefer: \"native-ws\",\n  apiBase: \"https://api.zunialab.com\",\n  metadata: { name: \"My dApp\", url: location.origin },\n});\n```\n\nOn React, `ConnectPairingModal` does this. On a phone browser it also offers a `zunia://connect?...` link so the user does not have to scan their own screen.\n\nAn unscanned code expires after 10 minutes. After the wallet sends `paired`, the room lasts 24 hours.\n\n## What travels\n\n```mermaid\nsequenceDiagram\n  participant Dapp as dApp\n  participant Relay as Relay\n  participant Wallet as Zunia mobile\n  Dapp->>Relay: POST /v1/connect/sessions (Origin recorded)\n  Relay-->>Dapp: sessionId, dappToken, walletJoinToken\n  Dapp->>Wallet: QR: sid, joinToken, dApp public key, relay\n  Wallet->>Relay: WS with joinToken (single use)\n  Relay-->>Wallet: resumeToken, verifiedOrigin\n  Wallet->>Dapp: hello with wallet public key\n  Note over Dapp,Wallet: HKDF, ChaCha20-Poly1305, sequence numbers\n  Dapp->>Wallet: connect_request (sealed)\n  Wallet->>Dapp: connect_approve (sealed)\n  Wallet->>Relay: paired\n```\n\nThe QR holds a one-time `walletJoinToken`, the dApp public key and the relay address. The `dappToken` never goes in the QR. Application frames are opaque to the relay. The protocol is in [zunia.connect.v2](https://docs.zunialab.com/docs/reference/connect-v2).\n\n## Verified origin\n\nWhen the page creates the session, the relay stores the browser `Origin` header as `verifiedOrigin` and gives it to the wallet. The phone shows that origin. If the dApp's metadata URL is a different site, the wallet warns (\"This site is not where it says\") and refuses sign-in.\n\n`verifiedOrigin` is only as trustworthy as the relay. A relay you do not control can invent one. Zunia mobile therefore only opens pairing URIs whose relay is on its **trusted list**: `wss://api.zunialab.com` by default, plus relays added at build time (`ZUNIA_CONNECT_TRUSTED_RELAYS`, `CONNECT_WS_PUBLIC_URL`) and, in debug builds, relays on the same machine or local network. A QR that names any other relay is refused.\n\nSo pass `apiBase: \"https://api.zunialab.com\"`. A relay you run yourself only pairs with a debug build of Zunia mobile, or with one you built with that relay on its list.\n\n## After a reload\n\n`session.restore({ apiBase, chains })` reopens the WebSocket with the stored dApp token and session keys. The wallet uses its `resumeToken`. Counters keep going, so a replayed frame is ignored.\n\n## Deep links\n\n| Kind | Example |\n|------|---------|\n| Custom scheme | `zunia://connect?…` |\n| Universal / App Link | `https://zunialab.com/connect?…` or `https://link.zunialab.com/connect?…` |\n\nThose HTTPS links do not open the app yet: the `apple-app-site-association` and `assetlinks.json` files on zunialab.com and link.zunialab.com still hold placeholders for the Apple team id and the Play signing certificate. Use the `zunia://` link.\n"
  },
  {
   "id": "integrate/walletconnect",
   "title": "WalletConnect",
   "description": "Use WalletConnect v2 when you want wallets other than Zunia. The Zunia mobile app can accept a WalletConnect session when a Cloud project id is set. It does not sign WalletConnect requests yet. To reach Zunia mobile, use QR pairing.",
   "url": "https://docs.zunialab.com/docs/integrate/walletconnect",
   "markdownUrl": "https://docs.zunialab.com/docs/integrate/walletconnect.md",
   "section": "Integrate",
   "optional": false,
   "headings": [
    "Same project id everywhere",
    "Deep links (mobile)"
   ],
   "markdown": "# WalletConnect\n\nUse WalletConnect v2 when you want wallets other than Zunia. The Zunia mobile app can **accept** a WalletConnect session when a Cloud project id is set. It does **not** sign WalletConnect requests yet. To reach Zunia mobile, use [QR pairing](https://docs.zunialab.com/docs/integrate/qr).\n\n```ts\nawait session.connect({\n  chains: [\"cosmoshub-4\"],\n  prefer: \"walletconnect\",\n  walletConnectProjectId: \"your WalletConnect Cloud project id\",\n  loadWalletConnect: () => import(\"@walletconnect/sign-client\"),\n});\n```\n\n`@walletconnect/sign-client` is an optional peer. Loading it through the function you pass keeps it out of apps that never use it. Show `session.pairing.uri` as a QR code the same way as native pairing.\n\nThe transport asks the wallet for accounts through `cosmos_getAccounts` (so CosmJS gets real public keys) and follows `session_event`, `session_update`, `session_delete` and `session_expire`.\n\n## Same project id everywhere\n\nCreate a project in the [Reown dashboard](https://dashboard.reown.com) (formerly WalletConnect Cloud) and use **one** id in:\n\n- your dApp (`walletConnectProjectId`)\n- `zunia-mobile` (`WALLETCONNECT_PROJECT_ID`)\n- `zunia-dashboard` (`NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID`). The dashboard keeps WalletConnect turned off until Zunia mobile signs WalletConnect requests.\n\nDo not commit a production project id in a public fork and then keep using it.\n\n## Deep links (mobile)\n\n| Kind | Example |\n|------|---------|\n| WalletConnect URI | `wc:…` |\n| Custom scheme | `zunia://wc?…` |\n| Universal / App Link | `https://zunialab.com/wc?…` |\n\nUntil WalletConnect signing ships in Zunia mobile, those links can open a session whose signature requests are never answered: they end in `TIMEOUT`. Prefer QR pairing for Zunia. The HTTPS link does not open the app yet either; see [Deep links](https://docs.zunialab.com/docs/integrate/qr#deep-links).\n"
  },
  {
   "id": "integrate/suggest-chain",
   "title": "Suggest a chain",
   "description": "If the wallet does not already know a chain, a site can propose it. The user sees Add <name> and the endpoints you chose. Nothing is saved until they approve.",
   "url": "https://docs.zunialab.com/docs/integrate/suggest-chain",
   "markdownUrl": "https://docs.zunialab.com/docs/integrate/suggest-chain.md",
   "section": "Integrate",
   "optional": false,
   "headings": [],
   "markdown": "# Suggest a chain\n\nIf the wallet does not already know a chain, a site can propose it. The user sees **Add &lt;name&gt;** and the endpoints you chose. Nothing is saved until they approve.\n\n```ts\nimport { getZunia } from \"@zunialab/sdk-web\";\n\nconst chain = {\n  chainId: \"my-test-1\",\n  chainName: \"My Testnet\",\n  rpc: \"https://rpc.example.com\",\n  rest: \"https://lcd.example.com\",\n  bip44: { coinType: 118 },\n  bech32Config: { bech32PrefixAccAddr: \"my\" },\n  currencies: [\n    { coinDenom: \"TEST\", coinMinimalDenom: \"utest\", coinDecimals: 6 },\n  ],\n  feeCurrencies: [\n    {\n      coinDenom: \"TEST\",\n      coinMinimalDenom: \"utest\",\n      coinDecimals: 6,\n      gasPriceStep: { low: 0.01, average: 0.025, high: 0.04 },\n    },\n  ],\n};\n\n// From a session connected to the extension\nawait session.suggestChain(chain);\n\n// Or straight to the provider, before any connection\nconst zunia = await getZunia();\nawait zunia?.experimentalSuggestChain?.(chain);\n```\n\n`session.suggestChain` works on the extension transport only. Over QR pairing or WalletConnect it throws `UNSUPPORTED`. Once the user has added the chain, call `connect` again with its chain id.\n\nThe shape is the usual Keplr `ChainInfo`. The wallet keeps a smaller draft: name, chain id, RPC, REST, bech32 prefix, coin type, denom metadata and a gas price. Endpoints must be `https://` and must not contain credentials.\n\nIf the chain is already in the bundled registry or was added earlier, the call returns without a prompt.\n\nA user can also add a network by hand: menu, **Manage networks**, then **Add chain manually**. See [Networks](https://docs.zunialab.com/docs/use-wallet/networks).\n\nOnly suggest endpoints you operate or trust. The wallet will send the user's addresses to them for balances and will broadcast through them when the user sends from the wallet itself.\n"
  },
  {
   "id": "integrate/flutter",
   "title": "Flutter",
   "description": "Two different things share the word \"SDK\" here. Keep them straight.",
   "url": "https://docs.zunialab.com/docs/integrate/flutter",
   "markdownUrl": "https://docs.zunialab.com/docs/integrate/flutter.md",
   "section": "Integrate",
   "optional": false,
   "headings": [
    "A Flutter **dApp** talking to Zunia",
    "The Zunia **wallet** (the mobile app)"
   ],
   "markdown": "# Flutter\n\nTwo different things share the word \"SDK\" here. Keep them straight.\n\n## A Flutter **dApp** talking to Zunia\n\nThe Dart package `zunia_sdk` in [zunia-sdk/flutter/zunia_sdk](https://github.com/Zunia-Lab/zunia-sdk/tree/main/flutter/zunia_sdk) is **not** a session client. It ships deep-link helpers and a Connect with Zunia button. A v2 client (QR pairing, events, CosmJS-equivalent signers) is planned.\n\n```yaml\ndependencies:\n  zunia_sdk:\n    git:\n      url: https://github.com/Zunia-Lab/zunia-sdk.git\n      path: flutter/zunia_sdk\n```\n\n```dart\nimport 'package:zunia_sdk/zunia_sdk.dart';\n\nConnectWithZuniaButton(\n  size: ConnectWithZuniaButtonSize.medium,\n  onPressed: () {\n    // Open your own WalletConnect or deep-link flow.\n  },\n);\n```\n\nUntil that client exists, a Flutter dApp should use WalletConnect with another wallet, or open a web view that uses `@zunialab/sdk-web`.\n\n## The Zunia **wallet** (the mobile app)\n\n[zunia-mobile](https://github.com/Zunia-Lab/zunia-mobile) is the wallet. It already speaks `zunia.connect.v2`: it reads the pairing URI, agrees on keys, shows the 6-digit code, shows the verified origin, binds sign-in to that origin, and resumes with `resumeToken`.\n\nIt accepts WalletConnect sessions when a Cloud project id is set. It does not sign WalletConnect requests yet.\n\nThe app is in App Store and Google Play review. To build it yourself, see [Install the mobile wallet](https://docs.zunialab.com/docs/use-wallet/mobile): it needs zunia-core and zunia-ui checked out next to it. Same recovery phrase as the extension if you want both devices.\n"
  },
  {
   "id": "integrate/troubleshooting",
   "title": "Troubleshooting",
   "description": "The extension is not in this browser, or the content script is not allowed on your origin.",
   "url": "https://docs.zunialab.com/docs/integrate/troubleshooting",
   "markdownUrl": "https://docs.zunialab.com/docs/integrate/troubleshooting.md",
   "section": "Integrate",
   "optional": false,
   "headings": [
    "`NOT_INSTALLED`",
    "The connect window never appears (Firefox, Safari)",
    "`LOCKED`",
    "`ORIGIN_MISMATCH` on sign-in",
    "Sign-in says \"The wallet returned no signature\"",
    "Events never arrive on Safari",
    "QR pairing never completes",
    "`SESSION_EXPIRED`",
    "CosmJS cannot sign",
    "`sendTx` is `UNSUPPORTED`",
    "`UNSUPPORTED` on a contract call or a memo with `&`",
    "Live balances stay off in Safari",
    "Packages 404 on npm"
   ],
   "markdown": "# Troubleshooting\n\n## `NOT_INSTALLED`\n\nThe extension is not in this browser, or the content script is not allowed on your origin.\n\n- Install Zunia from the [Chrome Web Store](https://chromewebstore.google.com/detail/zunia/ngokakoekdogobjmokipglbcclelgajk), or load a production build (`pnpm build:chrome`, then the unpacked `.output/chrome-mv3`).\n- On Safari, allow Zunia on the site. On iPhone and iPad: Settings, Apps, Safari, Extensions, Zunia, **Other Websites**: Allow. On a Mac: Safari Settings, Extensions, Zunia, **Always Allow on Every Website**, with Other Websites set to Allow. The provider only appears on sites Safari allows.\n- On Firefox, if the user withdrew host access in `about:addons`, the provider disappears.\n\n## The connect window never appears (Firefox, Safari)\n\nThat is expected. Those browsers answer connect from the **toolbar icon**. On Safari iOS, if `action.openPopup()` fails, the same screen opens in a tab.\n\n## `LOCKED`\n\nThe wallet stayed locked: the user closed the unlock window, or nobody unlocked it in time. Do not open a second connect prompt. `restore` after a reload reports `locked` without a window. Offer a button that calls `session.unlock()` (`unlock` on `useZuniaSession`): it opens the unlock window and keeps your site's grant.\n\n## `ORIGIN_MISMATCH` on sign-in\n\nThe message named a different host than the page that asked. `session.signIn()` fills `location.host` for you. Do not build the message yourself unless you also pass it through `checkSignInBinding`.\n\nOn QR pairing, the phone compares the message to the **verified origin**, not to your metadata URL.\n\n## Sign-in says \"The wallet returned no signature\"\n\nYou are talking to an old extension build that wrapped `signArbitrary` in an extra object. Current builds return the bare `StdSignature`, which is what Keplr, cosmos-kit, graz and the SDK expect. Update or rebuild the extension.\n\n## Events never arrive on Safari\n\nSafari drops `tabs.sendMessage` from the worker. Current builds send events over a port the content script opens. Rebuild from a revision that includes `lib/event-port.ts`. A backgrounded Safari tab reconnects when it becomes visible and replays what it missed.\n\n## QR pairing never completes\n\n- `apiBase` must be a relay the phone trusts: `https://api.zunialab.com`.\n- The phone refuses a QR whose relay is not on its trusted list. Debug builds also accept relays on the same machine or local network.\n- Compare the 6-digit codes. If they differ, throw the session away.\n- Unscanned rooms die after 10 minutes.\n\n## `SESSION_EXPIRED`\n\nThe relay forgot the room (restart without Postgres, or the 24-hour TTL). Call `connect` again.\n\n## CosmJS cannot sign\n\nPublic keys must be real `Uint8Array`s. The WalletConnect transport fetches them with `cosmos_getAccounts`. If you roll your own WC client and leave pubkeys empty, CosmJS will fail.\n\n## `sendTx` is `UNSUPPORTED`\n\nBy design. Sign with `signAmino` / `signDirect` and broadcast from the dApp.\n\n## `UNSUPPORTED` on a contract call or a memo with `&`\n\nCheck `error.reason`:\n\n- `blind-signing`: the extension could not decode the transaction, so it refused it without a prompt. Zunia 0.1.4 and older do this for a Direct contract call to a 32-byte contract. Hand CosmJS `session.getOfflineSignerFor(chainId, { messages, memo })` instead of `getOfflineSigner`, or ask the user to update Zunia.\n- `amino-escaping`: Zunia 0.1.4 or older signed `&`, `<` or `>` in a way the chain refuses. The SDK stopped it before broadcast. Ask the user to update Zunia, or remove those characters from the memo.\n\n`explainZuniaError(error)` gives a title and a next step for any error.\n\n## Live balances stay off in Safari\n\nSafari never prompts for `https://*/*`. On iPhone and iPad, the user allows it under Settings, Apps, Safari, Extensions, Zunia, Other Websites, Allow. On a Mac: Safari Settings, Extensions, Zunia, **Always Allow on Every Website**, with Other Websites set to Allow. The wallet says so when the switch is flipped.\n\n## Packages 404 on npm\n\n`@zunialab/sdk-web`, `@zunialab/sdk-react`, `@zunialab/sdk-core` and `@zunialab/interchain` are public. A 404 means the name or version does not match, or the install is using a registry other than `registry.npmjs.org`. See the [quickstart](https://docs.zunialab.com/docs/get-started/quickstart).\n"
  },
  {
   "id": "reference/provider",
   "title": "window.zunia",
   "description": "The extension injects this object on https:// pages and localhost. Types live in @zunialab/sdk-core as ZuniaProvider. An optional window.keplr alias exists when the user enables it.",
   "url": "https://docs.zunialab.com/docs/reference/provider",
   "markdownUrl": "https://docs.zunialab.com/docs/reference/provider.md",
   "section": "Reference",
   "optional": false,
   "headings": [
    "Methods",
    "Release and features",
    "Errors",
    "What the page never sees"
   ],
   "markdown": "# `window.zunia`\n\nThe extension injects this object on `https://` pages and localhost. Types live in `@zunialab/sdk-core` as `ZuniaProvider`. An optional `window.keplr` alias exists when the user enables it.\n\nThe provider dispatches `zunia#initialized` on `window`, and `zunia_keystorechange` on account changes (`keplr_keystorechange` too while the Keplr alias is on).\n\n## Methods\n\n| Method | |\n|--------|--|\n| `enable(chainIds)` | Ask for those chains. Prompts on first grant. |\n| `disable(chainIds?)` | Drop the grant for those chains, or the whole origin. |\n| `getConnectedChains()` | Chains this origin may use, no prompt. `[]` if none. |\n| `isLocked()` | Whether the wallet is locked. Connected sites only. |\n| `getKey(chainId)` | Keplr-shaped key for the active account. `NOT_CONNECTED` without a grant. Opens the unlock window if the wallet is locked. |\n| `getOfflineSigner(chainId)` | Amino + Direct signer. |\n| `getOfflineSignerOnlyAmino(chainId)` | Amino only. |\n| `getOfflineSignerAuto(chainId)` | Same as `getOfflineSigner` today. |\n| `signAmino(chainId, signer, signDoc)` | Amino signature. Always prompts. |\n| `signDirect(chainId, signer, signDoc)` | Direct signature. Always prompts. |\n| `signArbitrary(chainId, signer, data)` | ADR-036. Returns a bare `StdSignature`. Sign-in messages get a dedicated screen. |\n| `verifyArbitrary(chainId, signer, data, signature)` | Local check of an ADR-036 signature. |\n| `experimentalSuggestChain(chainInfo)` | Propose a chain. Prompts if unknown. |\n| `on` / `off` | See [events](https://docs.zunialab.com/docs/reference/events). |\n\n`sendTx` rejects with `UNSUPPORTED`. Sign, then broadcast from the dApp.\n\n`getKey` returns `{ name, algo, pubKey, address, bech32Address, isNanoLedger }`, plus `ethereumHexAddress` on chains with Ethereum-style addresses. `isNanoLedger` is always false.\n\n## Release and features\n\n`version` is the provider API version. It stays `\"0.1.0\"` in every release, so Zunia 0.1.0 to 0.1.4 cannot be told apart. From extension 0.1.5 the provider also has:\n\n- `extensionVersion`: the installed release.\n- `isZunia: true`, which tells Zunia's `window.keplr` alias from Keplr itself.\n- `features`: what this build signs that older builds refused or signed wrongly.\n\n| `features` entry | Meaning |\n|------------------|---------|\n| `sign-direct:wasm-contract-32` | Direct contract calls to 32-byte contracts are decoded and prompted. |\n| `sign-direct:send-32` | A Direct `MsgSend` to a 32-byte address is decoded and prompted. |\n| `sign-direct:osmosis-poolmanager` | Direct Osmosis poolmanager swaps that sell an exact amount. |\n| `sign-direct:osmosis-exact-out` | Direct Osmosis poolmanager swaps that buy an exact amount. |\n| `sign-amino:escaped` | Amino sign bytes escape `&`, `<`, `>`, U+2028 and U+2029 the way chains rebuild them. |\n| `sign-amino:osmosis-poolmanager` | Amino poolmanager swap requests are described and prompted. |\n\nA provider without `extensionVersion` is 0.1.4 or older. `zuniaCapabilities(window.zunia)` from `@zunialab/sdk-core` reads these fields, and `session.getOfflineSignerFor()` in `@zunialab/sdk-web` uses them to pick Direct or Amino.\n\n## Errors\n\nRejected calls use `ZuniaProviderError` with a stable `code`: `USER_REJECTED`, `NOT_CONNECTED`, `LOCKED`, `UNKNOWN_CHAIN`, `ORIGIN_MISMATCH`, `UNSUPPORTED`, `INVALID_PARAMS` or `INTERNAL`. Human messages keep Keplr's wording where they overlap, so existing integrations keep working. A request whose message types the wallet cannot decode is refused with `UNSUPPORTED` \"Blind signing disabled for unknown messages\", unless the user turned on blind signing.\n\n## What the page never sees\n\nThe seed phrase, the password, and private keys. Content scripts may only forward provider calls and ping the worker so it stays up while a request waits (on Safari, also while the page receives events). They cannot read `storage.session`.\n"
  },
  {
   "id": "reference/sdk",
   "title": "SDK API",
   "description": "Four packages. The first three share one session. ESM only.",
   "url": "https://docs.zunialab.com/docs/reference/sdk",
   "markdownUrl": "https://docs.zunialab.com/docs/reference/sdk.md",
   "section": "Reference",
   "optional": false,
   "headings": [
    "`@zunialab/sdk-web`",
    "`@zunialab/sdk-react`",
    "`@zunialab/sdk-core`",
    "`@zunialab/interchain`"
   ],
   "markdown": "# SDK API\n\nFour packages. The first three share one session. ESM only.\n\n## `@zunialab/sdk-web`\n\n| Export | |\n|--------|--|\n| `createZuniaSession()` | Session that picks a transport. |\n| `connectWithZunia(options)` | Create, connect, return the session. |\n| `restoreSession(options)` | Create and restore, or `null`. |\n| `createExtensionSession` / `createZuniaWsSession` / `createWalletConnectSession` | Force one transport. |\n| `getZunia` / `getZuniaSync` / `isZuniaInstalled` / `enableZunia` | Provider helpers. |\n| `renderQrSvg` / `qrMatrix` | Local QR rendering. |\n| `createConnectWithZuniaButton` | DOM button. CSS at `@zunialab/sdk-web/connect-button.css`. |\n| `explainZuniaError` / `zuniaCapabilities` / `zuniaSignMode` | Re-exported from `@zunialab/sdk-core`. |\n\n`ConnectOptions`: `chains`, `metadata`, `prefer` (`auto` \\| `extension` \\| `native-ws` \\| `walletconnect`), `apiBase` (the Zunia relay is `https://api.zunialab.com`), `wsBase`, `walletConnectProjectId`, `loadWalletConnect`, `timeoutMs`, `requestTimeoutMs`, `storage` (`null` disables persistence), `openInstallIfMissing`.\n\nA session exposes `connect`, `restore`, `disconnect`, `unlock`, `signIn`, `signAmino`, `signDirect`, `signArbitrary`, `suggestChain`, `getKey`, `getOfflineSigner`, `getOfflineSignerOnlyAmino`, `getOfflineSignerFor`, `capabilities`, `accounts`, `chains`, `status`, `transport`, `pairing`, `verificationCode`, `on` / `off`, `subscribe`, `getSnapshot`.\n\n## `@zunialab/sdk-react`\n\n| Export | |\n|--------|--|\n| `useZuniaSession(options?)` | Hook. Options `{ restore, session }`. Returns the snapshot plus `connected`, `locked`, `connecting`, `restoring`, `connect`, `disconnect`, `restore`, `unlock`, `signIn`, `session`. |\n| `useZunia()` | Detect `window.zunia`. |\n| `ConnectWithZuniaButton` | Official button. |\n| `ConnectPairingModal` | QR, then 6-digit code. |\n| `ZuniaQrCode` / `ZuniaMark` | Presentational. |\n| Re-exports | `createZuniaSession`, `ZuniaConnectError`, `explainZuniaError`, `createNonce`, … |\n\n## `@zunialab/sdk-core`\n\n| Export | |\n|--------|--|\n| `verifySignIn` / `createNonce` / `buildSignInMessage` / `parseSignInMessage` / `checkSignInBinding` | Sign-in. |\n| `ZuniaConnectError` / `ZuniaSignInError` / `explainZuniaError` | Errors, and words for them. |\n| `zuniaCapabilities` / `zuniaSignMode` / `aminoNeedsEscaping` / `ZUNIA_SIGNING_FEATURES` | What a Zunia extension can sign, and which mode to ask it for. |\n| `checkAminoSignature` / `serializeAminoSignDoc` | Whether an Amino signature is over the bytes the chain rebuilds. |\n| `ConnectCipher` / `deriveConnectKeys` / `verificationCodeFrom` / `parsePairingUri` / `buildPairingUri` | `zunia.connect.v2` crypto and URIs. |\n| `normalizeAminoResponse` / `normalizeDirectResponse` / `normalizeStdSignature` | Wire to CosmJS shapes. |\n| Types | `ZuniaProvider`, `ZuniaSession`, `ZuniaAccountInfo`, … |\n| Constants | `ZUNIA_CONNECT_PROTOCOL`, `ZUNIA_CONNECT_PATHS`, `ZUNIA_WALLETCONNECT`, deep links. |\n\nTest vectors: `test-vectors/sign-in-vectors.json` and `test-vectors/connect-v2-vectors.json` inside the package.\n\n## `@zunialab/interchain`\n\nIBC channel discovery, denom unwinding, packet-forward / ibc-hooks memos, cross-chain swaps and NFTs. Used by the wallet. A dApp can use it without talking to Zunia.\n"
  },
  {
   "id": "reference/events",
   "title": "Events",
   "description": "For older listeners, window also gets zunia_keystorechange, zunia_disconnect, zunia_locked and zunia_chainchanged, plus keplr_keystorechange and keplr_disconnect while the Keplr alias is on.",
   "url": "https://docs.zunialab.com/docs/reference/events",
   "markdownUrl": "https://docs.zunialab.com/docs/reference/events.md",
   "section": "Reference",
   "optional": false,
   "headings": [
    "Provider (`window.zunia`)",
    "Safari",
    "SDK session"
   ],
   "markdown": "# Events\n\n## Provider (`window.zunia`)\n\n| Event | Audience | Data |\n|-------|----------|------|\n| `accountsChanged` | Origins that are connected. Also sent when the wallet unlocks. | Read `getKey` again |\n| `chainChanged` | Origins that are connected | `{ chainIds }` still granted |\n| `disconnect` | Origins that lost access | `null` (all) or `{ chainIds }` |\n| `locked` | Connected origins | none |\n| `keplr_keystorechange` | Same as `accountsChanged` | none (Keplr alias) |\n\nFor older listeners, `window` also gets `zunia_keystorechange`, `zunia_disconnect`, `zunia_locked` and `zunia_chainchanged`, plus `keplr_keystorechange` and `keplr_disconnect` while the Keplr alias is on.\n\nThe worker only delivers these to tabs whose origin is granted. An origin that never connected hears nothing.\n\n### Safari\n\nSafari accepts `tabs.sendMessage` from the worker and never hands the message to the content script. The Safari build therefore:\n\n1. Lets the content script open a port named `zunia:provider-events` once the page has used the provider.\n2. Numbers every event and keeps the last 100 in `storage.session`.\n3. On reconnect (worker restarted, or the tab came back to the front), sends everything newer than the page's last number.\n\nA visible Safari tab that uses the wallet also pings the worker every 3 seconds so iOS does not stop it after ~8 seconds idle.\n\n## SDK session\n\n| Event | Payload |\n|-------|---------|\n| `status` | `ZuniaSessionStatus` |\n| `accountsChanged` | `ZuniaAccountInfo[]` |\n| `chainChanged` | `string[]` |\n| `pairing` | `{ transport, uri, expiresAt }` |\n| `verification` | 6-digit string (QR only) |\n| `disconnect` | reason string, such as `\"user\"` (your own `disconnect()`), `\"revoked\"` (the extension dropped your site) or `\"expired\"` (the relay session ran out) |\n| `error` | `ZuniaConnectError` |\n"
  },
  {
   "id": "reference/errors",
   "title": "Errors",
   "description": "Every transport rejects with ZuniaConnectError and a stable code. window.zunia uses the wallet's codes from this list: USERREJECTED to INVALIDPARAMS, and INTERNAL.",
   "url": "https://docs.zunialab.com/docs/reference/errors",
   "markdownUrl": "https://docs.zunialab.com/docs/reference/errors.md",
   "section": "Reference",
   "optional": false,
   "headings": [
    "Sign-in (`ZuniaSignInError`)"
   ],
   "markdown": "# Errors\n\nEvery transport rejects with `ZuniaConnectError` and a stable `code`. `window.zunia` uses the wallet's codes from this list: `USER_REJECTED` to `INVALID_PARAMS`, and `INTERNAL`.\n\n| Code | Meaning |\n|------|---------|\n| `USER_REJECTED` | The user said no, or closed the window. |\n| `NOT_CONNECTED` | This origin has no grant for that chain or account. |\n| `LOCKED` | The wallet is locked. |\n| `UNKNOWN_CHAIN` | The wallet does not know the chain. Suggest it first. |\n| `ORIGIN_MISMATCH` | A sign-in message names another site. |\n| `UNSUPPORTED` | The wallet or transport cannot do this (`sendTx`, unknown methods, blind signing of unknown messages). |\n| `INVALID_PARAMS` | Bad arguments. |\n| `NOT_INSTALLED` | No extension, relay or WalletConnect setup. |\n| `TIMEOUT` | The wallet did not answer in time. |\n| `DISCONNECTED` | The session ended while waiting. |\n| `PAIRING_FAILED` | QR pairing could not complete. |\n| `SESSION_EXPIRED` | The relay no longer knows the session. |\n| `NETWORK` | The relay or WalletConnect could not be reached. |\n| `INTERNAL` | Anything else. |\n\n`error instanceof ZuniaConnectError` and `error.code` are the stable checks. Do not match on `error.message`.\n\n`error.reason` narrows the cases where the next step differs:\n\n| Reason | Code | Meaning |\n|--------|------|---------|\n| `blind-signing` | `UNSUPPORTED` | This Zunia build cannot show the transaction, so it signed nothing. |\n| `amino-escaping` | `UNSUPPORTED` | Zunia 0.1.4 or older signed `&`, `<` or `>` in a way the chain refuses. Stopped before broadcast. |\n| `prompt-expired` | `TIMEOUT` | The extension's prompt expired before anyone answered. |\n| `stale-page` | `DISCONNECTED` | The extension was updated or reloaded under the page. Reload the page. |\n\n`explainZuniaError(error)` turns any error into a short `title`, a `message` with the next step, `retryable`, and the wallet's own text as `detail`.\n\n## Sign-in (`ZuniaSignInError`)\n\nThrown by `verifySignIn` on the server:\n\n`INVALID_MESSAGE`, `DOMAIN_MISMATCH`, `URI_MISMATCH`, `NONCE_MISMATCH`, `CHAIN_MISMATCH`, `ADDRESS_MISMATCH`, `ISSUED_IN_FUTURE`, `TOO_OLD`, `EXPIRED`, `NOT_YET_VALID`, `UNSUPPORTED_KEY`, `KEY_MISMATCH`, `INVALID_SIGNATURE`.\n"
  },
  {
   "id": "reference/connect-v2",
   "title": "zunia.connect.v2",
   "description": "Protocol between a dApp, a relay, and Zunia mobile. The TypeScript types and crypto live in @zunialab/sdk-core. The Dart wallet implements the same vectors.",
   "url": "https://docs.zunialab.com/docs/reference/connect-v2",
   "markdownUrl": "https://docs.zunialab.com/docs/reference/connect-v2.md",
   "section": "Reference",
   "optional": false,
   "headings": [
    "HTTP",
    "WebSocket",
    "Crypto",
    "Pairing URI",
    "What the relay sees"
   ],
   "markdown": "# `zunia.connect.v2`\n\nProtocol between a dApp, a relay, and Zunia mobile. The TypeScript types and crypto live in `@zunialab/sdk-core`. The Dart wallet implements the same vectors.\n\n## HTTP\n\nZunia runs a relay at `https://api.zunialab.com`. Any page origin may create sessions there; the relay records that origin, it does not filter on it.\n\n`POST /v1/connect/sessions` with the page's `Origin`. Response (`201`):\n\n```ts\n{\n  v: \"zunia.connect.v2\",\n  sessionId: string,\n  dappToken: string,        // stays with the dApp, never in the QR\n  walletJoinToken: string,  // single use, goes in the QR\n  verifiedOrigin: string | null,\n  expiresAt: number,\n  wsUrl: string,\n}\n```\n\n`GET` and `DELETE /v1/connect/sessions/{sessionId}` with `Authorization: Bearer <token>` read the session's state or end it.\n\n## WebSocket\n\n`/v1/connect/ws?sid=…&role=dapp` (or `role=wallet`). Tokens travel in `Sec-WebSocket-Protocol` (`zunia.connect.v2` plus `zunia.token.…`), not in the query string. The wallet's join token is consumed on first use and replaced by a `resumeToken`.\n\nRelay frames (`t`) are the only thing the relay reads: `welcome`, `hello`, `peer`, `msg`, `paired`, `ping` / `pong`, `error`, `closed`, and `close` from a client that ends the session. Application payloads sit inside sealed `msg` frames (`n` sequence, `c` ciphertext).\n\nOnly the wallet may send `hello` (its public key) and `paired`. `paired` locks the room and extends the TTL to 24 hours. Unpaired rooms die after 10 minutes. A socket is only replaced by a holder of that role's token.\n\n## Crypto\n\n- Key agreement: X25519\n- KDF: HKDF-SHA256 over a transcript of both public keys and the session id\n- Sealed frames: ChaCha20-Poly1305, with the sequence number in the AAD\n- Sequence numbers: monotonic, reject duplicates and replays\n- Verification code: 6 digits derived from the shared secret, shown on both sides\n\nTest vectors: `test-vectors/connect-v2-vectors.json` in the `@zunialab/sdk-core` package.\n\n## Pairing URI\n\n`zunia://connect?v=2&sid=…&t=…&pk=…&r=…` (also accepted as `https://zunialab.com/connect?…`). The query holds the protocol version, the session id (`sid`), the join token (`t`), the dApp public key (`pk`) and the relay's WebSocket base (`r`). It does not hold the dApp token.\n\n## What the relay sees\n\nSession id, hashed tokens, `verifiedOrigin`, role, timestamps, ciphertext length. Not keys, not accounts, not the sign-in message, not the transaction.\n\nTokens are stored as SHA-256 hashes and compared with `timingSafeEqual`. Sessions persist in Postgres when `DATABASE_URL` is set; otherwise they live in memory and die on restart. Run one instance: live sockets are not shared.\n\nLimits: 20 new sessions and 60 WebSocket upgrades per client address per minute, frames up to 512 KiB and 120 per 10 seconds per socket, 10,000 live sessions in all (beyond that, creation answers `503`), and a bounded offline queue. `TRUST_PROXY` must match how you terminate TLS, or rate limits attach to the wrong address.\n\nThis is not a multi-instance relay. The deployment at `api.zunialab.com` is one instance.\n"
  },
  {
   "id": "reference/sign-in",
   "title": "Sign-in message",
   "description": "CAIP-122-style text, signed as ADR-036. Built by buildSignInMessage / session.signIn(), parsed by parseSignInMessage, checked by verifySignIn.",
   "url": "https://docs.zunialab.com/docs/reference/sign-in",
   "markdownUrl": "https://docs.zunialab.com/docs/reference/sign-in.md",
   "section": "Reference",
   "optional": false,
   "headings": [],
   "markdown": "# Sign-in message\n\nCAIP-122-style text, signed as ADR-036. Built by `buildSignInMessage` / `session.signIn()`, parsed by `parseSignInMessage`, checked by `verifySignIn`.\n\n```text\n<domain> wants you to sign in with your Cosmos account:\n<address>\n\n<optional statement>\n\nURI: <uri>\nVersion: 1\nChain ID: <chain-id>\nNonce: <hex>\nIssued At: <ISO-8601>\nExpiration Time: <ISO-8601>   # optional, always set by session.signIn()\nNot Before: <ISO-8601>        # optional\nRequest ID: <text>            # optional\nResources:                    # optional\n- <uri>\n```\n\n| Field | Rule |\n|-------|------|\n| Domain | Exact host of the site that asked. The wallet compares it to the caller origin (extension) or to `verifiedOrigin` (QR). |\n| Address | Bech32 of the account that signs. Must derive from the signature's public key. |\n| URI | Must be on that domain (`https://app.example.com/…` is fine; `https://evil.example` is not). |\n| Version | `1` |\n| Chain ID | The chain the account belongs to. |\n| Nonce | 8 to 128 letters or digits. `createNonce()` gives 32 hex characters (128 random bits). Consume once on the server. |\n| Issued At | Not in the future (5 minutes of clock skew allowed). Default max age: 10 minutes (`maxAgeMs`). |\n| Expiration Time | Optional in the format. `session.signIn()` always sets it, by default 10 minutes after issue. Must be in the future at verify time. |\n\n`looksLikeSignIn` is what the wallet uses to decide between the sign-in screen and a generic arbitrary-data screen. `checkSignInBinding` is the domain/URI rule wallets must run before signing.\n\nVectors: `test-vectors/sign-in-vectors.json` in the `@zunialab/sdk-core` package.\n"
  },
  {
   "id": "security/model",
   "title": "Security model",
   "description": "Zunia is non-custodial. The seed phrase and private keys never leave the device that holds them. There is no Zunia server that can sign, recover a phrase, or freeze a balance.",
   "url": "https://docs.zunialab.com/docs/security/model",
   "markdownUrl": "https://docs.zunialab.com/docs/security/model.md",
   "section": "Security",
   "optional": false,
   "headings": [
    "Keys",
    "Permissions, per origin",
    "Approvals",
    "Sign-in binding",
    "QR pairing",
    "What leaves the device",
    "Limits (relay)",
    "Content Security Policy",
    "Supply chain",
    "Disclosure"
   ],
   "markdown": "# Security model\n\nZunia is non-custodial. The seed phrase and private keys never leave the device that holds them. There is no Zunia server that can sign, recover a phrase, or freeze a balance.\n\nNo independent audit has been completed. Do not read this page as one.\n\n## Keys\n\n- Generated or imported on the device (BIP-39, BIP-44, coin type 118 by default, coin type 60 for Ethermint-style addresses).\n- Encrypted at rest with the user's password. The unlocked phrase lives in `storage.session` in the extension (cleared when the browser closes) and in the platform secure store on mobile.\n- The extension asks `storage.session` to stay out of content-script reach (`TRUSTED_CONTEXTS`).\n- Hardware wallets are not supported.\n\n## Permissions, per origin\n\nA site sees nothing until the user approves `enable` / `connect` for named chains. Grants are stored per origin and last 7 days. The user revokes them under **Connected dApps** (menu in the extension). An expired or revoked grant emits `disconnect` to that origin only.\n\n`getConnectedChains()` returns the current grant without unlocking and without opening a window.\n\n## Approvals\n\nEvery signature opens a screen: connect, sign-in, Amino, Direct, arbitrary data, or suggest-chain. Message types the wallet cannot decode are refused without a prompt (`UNSUPPORTED`), unless the user turns on **Blind signing** under **Settings & security** → **Security**. It is off by default. The user can reject or close the window (`USER_REJECTED`).\n\nFirefox and Safari answer connect from the toolbar popup. Safari on iOS may open the same screen in a tab.\n\n## Sign-in binding\n\nA Sign in with Zunia message must name the site that asked. The extension compares domain and URI to the caller origin and refuses otherwise (`ORIGIN_MISMATCH`), without opening a window for a spoofed domain. `verifySignIn` repeats those checks on your server, plus the key, the address, the nonce and the dates. Consume each nonce once.\n\n## QR pairing\n\nMessages between the page and the phone are X25519 + HKDF-SHA256 + ChaCha20-Poly1305, with sequence numbers. The relay sees ciphertext and routing metadata. The QR join token is single use. The dApp token never goes in the QR.\n\n`verifiedOrigin` is the `Origin` header the relay recorded when the page created the session. The phone shows it and binds sign-in to it. That value is only as good as the relay: Zunia mobile therefore refuses pairing URIs whose relay is not on its trusted list. By default that list holds only Zunia's relay, `wss://api.zunialab.com`. See [QR connection](https://docs.zunialab.com/docs/integrate/qr#verified-origin).\n\n## What leaves the device\n\nWhen live balances are on, addresses go to the chain endpoints in the catalog (or the ones a user / dApp added), and the NFT screen sends a Stargaze address to Stargaze's API to list its collections. Prices come from CoinGecko and swap quotes from Osmosis's SQS service, without an address. Signed transactions go to the node that broadcasts them. The extension sends nothing to a Zunia server.\n\nFirefox's data-collection prompt is answered as `financialAndPaymentInfo` for that reason.\n\nQR pairing goes through Zunia's relay at `api.zunialab.com`, which sees ciphertext and routing metadata, as above. The web dashboard at `app.zunialab.com` reads chain data and broadcasts through its own server, so the addresses you use there and the transactions you sign there reach that server. It never receives a key or a phrase.\n\n## Limits (relay)\n\nSession creation per client address, frame size and rate, a global room cap, hashed tokens, `timingSafeEqual`. `TRUST_PROXY` must match your edge, or the limiter attaches to the wrong address. One instance only.\n\n## Content Security Policy\n\nThe extension pages use `script-src 'self'` plus `'wasm-unsafe-eval'` for the kernel, `object-src 'self'`, and `frame-ancestors 'self' https: http://localhost:* http://127.0.0.1:*` so the connect prompt can be drawn in the page. Only that prompt (`connect.html`) is web accessible, so it is the only extension page a site can frame; the popup and every signing screen are not. The provider is injected through a web-accessible script. dApps should keep their own CSP strict: the SDK's `localStorage` keys are readable by any script on the origin.\n\n## Supply chain\n\nRepositories are public under [github.com/Zunia-Lab](https://github.com/Zunia-Lab). Extension builds are checked in CI (MV3, WASM present, no bare specifiers). Bit-for-bit reproducible store builds are **not** claimed yet. See [Reproducible builds](https://docs.zunialab.com/docs/developers/reproducible-builds).\n\n## Disclosure\n\nEmail [dev@zunialab.com](mailto:dev@zunialab.com). Do not open a public issue for a vulnerability. See [SECURITY.md](https://github.com/Zunia-Lab/.github/blob/main/SECURITY.md).\n"
  },
  {
   "id": "use-wallet/extension",
   "title": "Install the extension",
   "description": "On Chrome, install Zunia from the Chrome Web Store.",
   "url": "https://docs.zunialab.com/docs/use-wallet/extension",
   "markdownUrl": "https://docs.zunialab.com/docs/use-wallet/extension.md",
   "section": "Use the wallet",
   "optional": false,
   "headings": [
    "Daily use",
    "Browser notes"
   ],
   "markdown": "# Install the extension\n\nOn Chrome, install Zunia from the [Chrome Web Store](https://chromewebstore.google.com/detail/zunia/ngokakoekdogobjmokipglbcclelgajk).\n\nSafari is in App Store review and Firefox is planned. Edge Add-ons has no listing. For those browsers, load a build you made. The extension links zunia-core (built to WebAssembly with Rust and `wasm-bindgen-cli`), zunia-ui and zunia-sdk from checkouts next to it, so build those first:\n\n```bash\ngit clone https://github.com/Zunia-Lab/zunia-core\ngit clone https://github.com/Zunia-Lab/zunia-ui\ngit clone https://github.com/Zunia-Lab/zunia-sdk\ngit clone https://github.com/Zunia-Lab/zunia-extension\n\n(cd zunia-core && ./scripts/build-wasm.sh)\n(cd zunia-ui && pnpm install && pnpm --filter @zunialab/tokens build && pnpm --filter @zunialab/ui build)\n(cd zunia-sdk && pnpm install && pnpm --filter @zunialab/interchain build)\n\ncd zunia-extension\npnpm install\npnpm build\n```\n\n| Browser | Load |\n|---------|------|\n| Chrome, Brave, Opera | `chrome://extensions` → Developer mode → Load unpacked → `.output/chrome-mv3` |\n| Edge | `edge://extensions` → the same, or `.output/edge-mv3` |\n| Firefox 140+ | `about:debugging#/runtime/this-firefox` → Load Temporary Add-on → `.output/firefox-mv3/manifest.json` |\n| Safari (macOS) | `pnpm safari:build`, run the macOS scheme in Xcode, then in Safari: Develop → Developer Settings → **Allow unsigned extensions**, enable Zunia, allow it on all websites |\n| Safari (iOS Simulator) | `pnpm safari:build`, run the iOS scheme, then Settings → Apps → Safari → Extensions → Zunia, allow on all websites |\n\n`pnpm dev` rebuilds Chrome while you edit. Requirements: Node 22 and pnpm 9 (`corepack enable`). See the [extension README](https://github.com/Zunia-Lab/zunia-extension) for the kernel toolchain.\n\nOn first open you get **Welcome**, then **Create wallet** or **Restore with phrase**. Write the recovery phrase down offline. Zunia cannot restore it.\n\n## Daily use\n\nThe toolbar icon opens the popup (a sheet on iPhone). The bottom bar has **Home**, **Earn**, **Swap** and **Activity**. The home screen has **Send**, **Deposit**, **Swap** and **Stake**. The header opens the account switcher. The menu (☰) holds:\n\n- **Manage networks**\n- **NFTs**\n- **Governance**\n- **Notifications**\n- **Address book**\n- **Connected dApps** (revoke a site or a chain)\n- **Settings & security** (accounts, networks, security, preferences, notifications)\n- **Lock wallet**\n\n**Settings & security** → **Security** is where you turn the Keplr-compatible alias (**Keplr alias**) on or off.\n\n## Browser notes\n\n- **Firefox and Safari:** a dApp connect request waits in this popup. Click the toolbar icon.\n- **Safari on iPhone:** if the popup cannot open, the request opens in a tab. Inputs are 16px so Safari does not zoom. Approve / Reject sit under the content so they stay in the half-height sheet.\n- **Safari website access:** on iPhone and iPad, Settings → Apps → Safari → Extensions → Zunia → Other Websites → Allow. On a Mac, Safari → Settings → Extensions → Zunia → Always Allow on Every Website, with Other Websites set to Allow. Without that, sites do not see `window.zunia`, and live balances stay off.\n- Auto-lock counts time without input. Opening the popup resets it. Closing the browser clears the unlocked session.\n\nMore detail: [browsers.md](https://github.com/Zunia-Lab/zunia-extension/blob/main/docs/browsers.md) in the extension repo.\n"
  },
  {
   "id": "use-wallet/mobile",
   "title": "Install the mobile wallet",
   "description": "The iOS and Android apps are in App Store and Google Play review. Until they are listed, build them yourself with a Flutter release that ships Dart 3.11.5 or later (the repo builds with Flutter 3.41).",
   "url": "https://docs.zunialab.com/docs/use-wallet/mobile",
   "markdownUrl": "https://docs.zunialab.com/docs/use-wallet/mobile.md",
   "section": "Use the wallet",
   "optional": false,
   "headings": [
    "Pair with a website",
    "Same phrase, two devices"
   ],
   "markdown": "# Install the mobile wallet\n\nThe iOS and Android apps are in App Store and Google Play review. Until they are listed, build them yourself with a Flutter release that ships Dart 3.11.5 or later (the repo builds with Flutter 3.41).\n\nThe app links zunia-core and zunia-ui from checkouts next to it, and needs the signing kernel's native libraries from a [zunia-core release](https://github.com/Zunia-Lab/zunia-core/releases). `scripts/fetch-native.sh` downloads and checks them; it needs the GitHub CLI (`gh`).\n\n```bash\ngit clone https://github.com/Zunia-Lab/zunia-core.git\ngit clone https://github.com/Zunia-Lab/zunia-ui.git\ngit clone https://github.com/Zunia-Lab/zunia-mobile.git\ncd zunia-mobile\nZUNIA_CORE_TAG=<zunia-core release tag> ./scripts/fetch-native.sh\nflutter pub get\nflutter run\n```\n\nRelease builds:\n\n```bash\nflutter build apk --release\nflutter build appbundle --release\nflutter build ipa --release   # needs an Apple team in Xcode\n```\n\nCreate a wallet or import the same 12/24-word phrase you use in the extension. Keys stay on the phone. Zunia does not sync them through a server.\n\n## Pair with a website\n\n1. On the site, choose Connect with Zunia (QR).\n2. Scan the code, or tap the deep link if you are already on the phone.\n3. Check that the 6-digit code matches the site.\n4. Read the origin the relay confirmed. If it differs from the name the site used, stop.\n5. Approve the chains. Later signatures open a sheet each time.\n\nPairing goes through Zunia's relay at `api.zunialab.com`, the only relay the app trusts by default. A code that names another relay is refused. Debug builds also accept a relay on the same machine or local network.\n\nPaired sites are listed under **Settings & security** → **Connected apps**, where you can end a session.\n\nWalletConnect QR codes can open a session. The app does not sign those requests yet.\n\n## Same phrase, two devices\n\nImport the phrase on the second device. Each device holds its own encrypted copy. Losing both copies without the phrase means the accounts are gone. Accounts added after the first one do not carry over between the extension and the app; see [More accounts](https://docs.zunialab.com/docs/use-wallet/keys#more-accounts).\n"
  },
  {
   "id": "use-wallet/keys",
   "title": "Keys and accounts",
   "description": "Zunia is an HD wallet (BIP-39 / BIP-44). Each account has a bech32 address per network.",
   "url": "https://docs.zunialab.com/docs/use-wallet/keys",
   "markdownUrl": "https://docs.zunialab.com/docs/use-wallet/keys.md",
   "section": "Use the wallet",
   "optional": false,
   "headings": [
    "Create",
    "Import",
    "More accounts",
    "Reveal the phrase",
    "Backup"
   ],
   "markdown": "# Keys and accounts\n\nZunia is an HD wallet (BIP-39 / BIP-44). Each account has a bech32 address per network.\n\n- Default coin type **118** (Cosmos). A chain in the catalog can set another.\n- Coin type **60** for Ethermint-style addresses. That is address derivation only. Zunia is not an Ethereum wallet.\n\n## Create\n\nOnboarding → **Create wallet** → write the phrase on paper → confirm some of its words → set a name and a password → choose networks. The phrase is shown once during setup. Screenshots are a bad idea.\n\n## Import\n\nOnboarding → **Restore with phrase** in the extension, **I have a recovery phrase** in the mobile app. 12 or 24 words. The password only unlocks this device; it is not part of the phrase.\n\n## More accounts\n\nThe two apps add accounts differently:\n\n- **Extension:** account name (top left) → **Add account** → **Create new account** or **Restore with phrase**. Each account added this way has its own recovery phrase. The same password unlocks them all. Back up each phrase.\n- **Mobile app:** **Settings & security** → **Wallets** → **Add account**. It derives the next account from the same phrase, so backing up that phrase backs up all of them.\n\nSo only the first account of a phrase appears in both apps when you import that phrase. Switch accounts from the account list; a connected site hears `accountsChanged`.\n\n## Reveal the phrase\n\n**Settings & security** → **Security** → **Reveal recovery phrase** (password again). There is no way to change the password. **Forgot password?** on the extension's unlock screen removes the wallet so you can restore it from the phrase. It only helps if you still have that phrase, and the phrase of each account you added with its own.\n\n## Backup\n\nZunia cannot recover a lost phrase. Store it offline. Anyone with the phrase has the accounts.\n\nLedger and Keystone are planned, not available.\n"
  },
  {
   "id": "use-wallet/networks",
   "title": "Networks",
   "description": "Home lists the networks you turned on. You choose them when you create or restore a wallet (Choose networks). The bundled catalog comes from zunia-chain-registry. Updates to that repo ship in the next wallet build.",
   "url": "https://docs.zunialab.com/docs/use-wallet/networks",
   "markdownUrl": "https://docs.zunialab.com/docs/use-wallet/networks.md",
   "section": "Use the wallet",
   "optional": false,
   "headings": [
    "Manage networks",
    "Live balances",
    "Fees"
   ],
   "markdown": "# Networks\n\nHome lists the networks you turned on. You choose them when you create or restore a wallet (**Choose networks**). The bundled catalog comes from [zunia-chain-registry](https://github.com/Zunia-Lab/zunia-chain-registry). Updates to that repo ship in the next wallet build.\n\n## Manage networks\n\nMenu → **Manage networks**. Turn each network on or off for the current account, or add one the catalog does not have with **Add chain manually**.\n\n**Add network** has two tabs. **Registry** searches the catalog. **Manual** asks for chain name, chain ID, RPC, REST, prefix, coin type, base denom and gas price; the token gets 6 decimals. RPC and REST must be `https://`. Only add endpoints you trust: they will see your addresses and can show fake balances or refuse to broadcast.\n\nA dApp can propose a chain too ([suggest a chain](https://docs.zunialab.com/docs/integrate/suggest-chain)). You still have to approve it.\n\n## Live balances\n\nWhile live balances are off, Home shows **Turn on live balances** with an **Enable** button. The switch itself is **Settings & security** → **Preferences** → **Live balances**. When it is on, the wallet reads spendable and staked amounts from each chain's public LCD.\n\n- Chrome and Firefox prompt once for host access.\n- Safari never prompts. On iPhone and iPad, allow **Other Websites** under Settings → Apps → Safari → Extensions → Zunia. On a Mac, Safari → Settings → Extensions → Zunia → **Always Allow on Every Website**, with Other Websites set to Allow. Then try the switch again.\n\nTurn it off and the wallet stops those requests.\n\n## Fees\n\nNetwork fees go to validators, not to Zunia. Before you sign, the screen shows the fee denom, the amount and the gas. The network fee row on the Send and Swap screens opens the speed (**Low**, **Mid**, **High**) and the gas adjustment, and Zunia keeps your choice. Defaults come from the registry.\n\nA swap made with the **Swap** screen of the extension or of the [web dashboard](https://app.zunialab.com) also carries a 0.5% Zunia commission. See [Fees](https://docs.zunialab.com/docs/use-wallet/fees).\n"
  },
  {
   "id": "use-wallet/send-and-ibc",
   "title": "Send and IBC",
   "description": "Home → Send. Pick the token, the recipient and the amount. Same-chain sends stay on that network. If the recipient is on another catalog chain, the wallet builds an IBC transfer (including packet-forward when the route needs a hop).",
   "url": "https://docs.zunialab.com/docs/use-wallet/send-and-ibc",
   "markdownUrl": "https://docs.zunialab.com/docs/use-wallet/send-and-ibc.md",
   "section": "Use the wallet",
   "optional": false,
   "headings": [],
   "markdown": "# Send and IBC\n\nHome → **Send**. Pick the token, the recipient and the amount. Same-chain sends stay on that network. If the recipient is on another catalog chain, the wallet builds an IBC transfer (including packet-forward when the route needs a hop).\n\nThe review screen shows:\n\n- Source token and amount\n- Destination chain and address\n- Route (channels)\n- Fee on the source chain\n- Memo, if you set one\n\nSign on the device. The wallet broadcasts from the LCD you configured for that chain. Arrival time is whatever the IBC relayers take; Zunia does not run those.\n\n**Deposit** (**Receive** in the mobile app) shows the current account's address and QR for the selected network. Check the prefix (`cosmos`, `osmo`, …) before you give it out.\n\n**Address book** (menu) stores labels so you do not paste a raw bech32 every time.\n\n**Swap** is a separate home action. It uses the same review + sign path. It needs the WASM kernel; if the kernel failed to load, Confirm stays disabled and the screen says why.\n\nA swap made with **Swap** carries a 0.5% Zunia commission, taken in the same transaction from the token you sell. The review screen shows it before you sign. Sends and IBC transfers carry no commission. [Fees](https://docs.zunialab.com/docs/use-wallet/fees) covers swaps that start on another chain.\n\nDo not paste a seed phrase into any website, including the Zunia web dashboard at `app.zunialab.com`. It never asks for one: it holds no keys and sends every transaction to your wallet to sign.\n"
  },
  {
   "id": "use-wallet/staking",
   "title": "Staking and governance",
   "description": "Home → Stake opens Earn: validators, commission, your delegations and rewards.",
   "url": "https://docs.zunialab.com/docs/use-wallet/staking",
   "markdownUrl": "https://docs.zunialab.com/docs/use-wallet/staking.md",
   "section": "Use the wallet",
   "optional": false,
   "headings": [],
   "markdown": "# Staking and governance\n\nHome → **Stake** opens Earn: validators, commission, your delegations and rewards.\n\n- **Stake** with a validator from its row.\n- **Unstake** from a delegation (extension only). Unbonding time is the chain's (often 14 or 21 days).\n- **Claim rewards** from Earn.\n\nNeither app redelegates. To move stake, unstake, wait out the unbonding time, then stake again.\n\nEach action has its own review screen (amount, fee, validator). Sign on the device.\n\nHome → menu → **Governance** lists proposals on the selected network. You can vote **Yes**, **No**, **Veto** (no with veto) or **Abstain**. The wallet does not deposit for you.\n\nStaking parameters (minimum, unbonding, APR figures) come from the chain. Zunia takes no cut of rewards.\n"
  },
  {
   "id": "use-wallet/fees",
   "title": "Fees",
   "description": "The network fee every transaction pays, and the 0.5% commission on swaps made with Zunia's own Swap screen in the extension or on the web dashboard.",
   "url": "https://docs.zunialab.com/docs/use-wallet/fees",
   "markdownUrl": "https://docs.zunialab.com/docs/use-wallet/fees.md",
   "section": "Use the wallet",
   "optional": false,
   "headings": [
    "Network fee",
    "Swap commission",
    "Swaps that start on another chain",
    "No commission"
   ],
   "markdown": "# Fees\n\nThe wallet is free to install. There is no premium tier and no token.\n\n| Action | Network fee | Zunia commission |\n|--------|-------------|------------------|\n| Send, IBC transfer | Yes | None |\n| Stake, unstake, claim rewards, vote | Yes | None |\n| Swap with the **Swap** screen of the extension or of the [web dashboard](https://app.zunialab.com) | Yes | 0.5% |\n| Swap on another site or dApp, signed with Zunia | Yes | None |\n\n## Network fee\n\nEvery transaction pays a network fee to the chain. It goes to validators, not to Zunia. Before you sign, the screen shows the fee denom, the amount and the gas. Gas settings are under [Networks](https://docs.zunialab.com/docs/use-wallet/networks#fees).\n\n## Swap commission\n\nA swap made with the **Swap** screen of the Zunia browser extension, or of the web dashboard at app.zunialab.com, carries a 0.5% commission, sent to Zunia. Both charge the same rate to the same Zunia addresses.\n\n- It is taken in the same transaction as the swap.\n- It is paid in the token you sell.\n- The review screen shows it before you sign.\n\nOn a chain where Zunia has no fee address, the swap carries no commission.\n\n## Swaps that start on another chain\n\nWhen the funds sit on another chain, they are transferred to Osmosis and swapped there. The commission is taken on the starting chain, when that transfer is sent.\n\nIf the swap then fails on Osmosis, the amount swapped comes back. The commission does not.\n\nSome pairs cannot be swapped in one step from another chain. The **Swap** screen then asks you to move the tokens to Osmosis first: with Send in the extension, with Bridge on the web dashboard. That transfer carries no commission. The swap that follows starts on Osmosis, and its commission is taken there, in the same transaction.\n\n## No commission\n\nSends, IBC transfers and staking carry no Zunia commission. You pay only the network fee. Zunia takes no cut of staking rewards.\n\nA swap you make on another site or dApp and only sign with Zunia carries no Zunia commission.\n\nThe commission shown next to a validator in Earn is set by that validator. It is not Zunia's.\n"
  },
  {
   "id": "chain-registry/overview",
   "title": "Chain registry overview",
   "description": "The zunia-chain-registry repository holds JSON metadata for Cosmos-SDK, EVM, and SVM chains.",
   "url": "https://docs.zunialab.com/docs/chain-registry/overview",
   "markdownUrl": "https://docs.zunialab.com/docs/chain-registry/overview.md",
   "section": "Chain registry",
   "optional": false,
   "headings": [],
   "markdown": "# Chain registry overview\n\nThe [zunia-chain-registry](https://github.com/Zunia-Lab/zunia-chain-registry) repository holds JSON metadata for Cosmos-SDK, EVM, and SVM chains.\n\nThe extension, the mobile app and the web dashboard bundle the Cosmos entries when they are built, so a registry change reaches users with their next release. Only Cosmos chains are signed for: EVM and SVM entries are metadata, and no Zunia app signs EVM or Solana transactions.\n\nEach entry, and its logo, is also served from:\n\n```\nhttps://raw.githubusercontent.com/Zunia-Lab/zunia-chain-registry/main/cosmos/{identifier}.json\nhttps://raw.githubusercontent.com/Zunia-Lab/zunia-chain-registry/main/images/{identifier}/chain.png\n```\n\nThe schema is compatible with the wider Cosmos suggest-chain ecosystem. Upstream reference: [keplr-chain-registry](https://github.com/chainapsis/keplr-chain-registry). Curation and branding are maintained by Zunia Lab.\n"
  },
  {
   "id": "chain-registry/contribute",
   "title": "Contribute a chain",
   "description": "See the repository README for field requirements and the features array reference.",
   "url": "https://docs.zunialab.com/docs/chain-registry/contribute",
   "markdownUrl": "https://docs.zunialab.com/docs/chain-registry/contribute.md",
   "section": "Chain registry",
   "optional": false,
   "headings": [],
   "markdown": "# Contribute a chain\n\n1. Fork [zunia-chain-registry](https://github.com/Zunia-Lab/zunia-chain-registry)\n2. Add `{chain-identifier}.json` under `cosmos/`, `evm/`, or `svm/`\n3. Add `images/{chain-identifier}/chain.png` (256x256)\n4. Run `yarn validate path/to/file.json`\n5. Open a pull request\n\nSee the repository README for field requirements and the `features` array reference.\n"
  },
  {
   "id": "brand/assets",
   "title": "Brand assets",
   "description": "Official logos, colors, and typography live in zunia-brand.",
   "url": "https://docs.zunialab.com/docs/brand/assets",
   "markdownUrl": "https://docs.zunialab.com/docs/brand/assets.md",
   "section": "Brand",
   "optional": true,
   "headings": [
    "Colors",
    "Fonts",
    "Docs site assets",
    "Usage"
   ],
   "markdown": "# Brand assets\n\nOfficial logos, colors, and typography live in [zunia-brand](https://github.com/Zunia-Lab/zunia-brand).\n\n## Colors\n\n| Name | Hex |\n|------|-----|\n| Accent red | `#FF1B0C` |\n| Vermilion | `#FF4E12` |\n| Amber | `#FF8A17` |\n| Gold | `#FFBE14` |\n| Ink | `#111111` |\n| Paper | `#F1F0EE` |\n| Void | `#0B0A09` |\n\n## Fonts\n\n- Space Grotesk (UI, headings)\n- JetBrains Mono (addresses, chain IDs)\n\n## Docs site assets\n\nThis site ships the docs subbrand lockups and mark under `/img/`:\n\n- `zunia-docs-white.svg` / `zunia-docs-black.svg` (navbar)\n- `zunia-mark.svg` (`currentColor` mark)\n- `favicon.ico`, `zunia-social-card.png`\n\n## Usage\n\nDo not modify the mark, apply gradients to the mark, or change wordmark spacing. See the brand README for full guidelines.\n\nQuestions: [zunialab@gmail.com](mailto:zunialab@gmail.com) or [dev@zunialab.com](mailto:dev@zunialab.com)\n"
  },
  {
   "id": "legal/terms",
   "title": "Terms of Service",
   "description": "Status: DRAFT — not legal advice. Not in force until counsel review and publication.",
   "url": "https://docs.zunialab.com/docs/legal/terms",
   "markdownUrl": "https://docs.zunialab.com/docs/legal/terms.md",
   "section": "Legal (draft)",
   "optional": true,
   "headings": [
    "1. Parties",
    "2. Non-custodial software",
    "3. Eligibility / acceptable use",
    "4. Risks",
    "5. Fees",
    "6. Disclaimers",
    "7. Contact"
   ],
   "markdown": "# Terms of Service\n\n**Status: DRAFT — not legal advice. Not in force until counsel review and publication.**\n\nLast updated: 14 October 2026 (draft)\n\n## 1. Parties\n\nThese draft terms would govern use of Zunia wallets, websites, and related services operated by Zunia Lab (name TBD).\n\n## 2. Non-custodial software\n\nZunia provides non-custodial wallet software. You control your keys. We cannot recover seed phrases, reverse transactions, or freeze on-chain balances.\n\n## 3. Eligibility / acceptable use\n\nDraft placeholders: age requirements, prohibited illicit use, sanctions compliance, no reverse engineering of security controls beyond research disclosure policy.\n\n## 4. Risks\n\nBlockchain transactions are irreversible. Smart contract, bridge, and third-party dApp risk sits with you.\n\n## 5. Fees\n\nThe wallet is free to install, with no premium tier and no token. Network fees are paid to validators / chains, not to Zunia. Sends, IBC transfers and staking carry no Zunia commission: you pay only the network fee.\n\nSwaps made with the Swap screen of the Zunia browser extension or of the Zunia web dashboard (app.zunialab.com) carry a 0.5% commission, sent to Zunia. It is taken in the same transaction as the swap, in the token you sell, and shown before you sign. For a cross-chain swap (funds on another chain, swapped on Osmosis), the commission is taken on the starting chain when the transfer is sent. If the swap then fails on Osmosis, the amount swapped comes back; the commission does not. Swaps made on another website or dApp and only signed with Zunia carry no Zunia commission.\n\n## 6. Disclaimers\n\nSoftware provided \"as is\" while in beta / pre-release. Limit liability clauses TBD by counsel.\n\n## 7. Contact\n\ndev@zunialab.com\n"
  },
  {
   "id": "legal/privacy",
   "title": "Privacy Policy",
   "description": "Status: DRAFT — not legal advice. Not in force until counsel review and publication.",
   "url": "https://docs.zunialab.com/docs/legal/privacy",
   "markdownUrl": "https://docs.zunialab.com/docs/legal/privacy.md",
   "section": "Legal (draft)",
   "optional": true,
   "headings": [
    "Summary",
    "Data we may process (when features ship)",
    "What we do not collect by design",
    "Retention / deletion",
    "GDPR / rights"
   ],
   "markdown": "# Privacy Policy\n\n**Status: DRAFT — not legal advice. Not in force until counsel review and publication.**\n\nLast updated: 2026-08-31 (draft)\n\n## Summary\n\nZunia is designed so **seed phrases and private keys never leave your device** and are never uploaded to Zunia servers. The web dashboard does not accept signing keys in the browser.\n\n## Data we may process (when features ship)\n\n| Category | Examples | Purpose |\n|----------|----------|---------|\n| Device / push | Web Push subscription endpoints, FCM tokens | Deliver tx / security alerts |\n| Platform session | Address + ADR-36 device binding proof | Authorize push and light history |\n| Usage (optional) | Crash reports with scrubbing | Reliability |\n| Support | Email you send us | Respond to requests |\n\n## What we do not collect by design\n\n- Seed phrases, private keys, mnemonics\n- Full wallet unlock passwords\n- Unnecessary chain RPC traffic from the browser (dashboard proxies server-side)\n\n## Retention / deletion\n\nTBD — push tokens deleted on unsubscribe / device revoke; support mail per ordinary retention.\n\n## GDPR / rights\n\nSee [GDPR data map](https://docs.zunialab.com/docs/legal/gdpr-data-map). Contact: dev@zunialab.com\n"
  },
  {
   "id": "legal/gdpr-data-map",
   "title": "GDPR data map",
   "description": "Status: DRAFT. Map systems before appointing a DPO / completing RoPA.",
   "url": "https://docs.zunialab.com/docs/legal/gdpr-data-map",
   "markdownUrl": "https://docs.zunialab.com/docs/legal/gdpr-data-map.md",
   "section": "Legal (draft)",
   "optional": true,
   "headings": [
    "Cross-border",
    "Data subject requests"
   ],
   "markdown": "# GDPR data map (draft)\n\n**Status: DRAFT.** Map systems before appointing a DPO / completing RoPA.\n\n| Processing | Personal data | Lawful basis (draft) | Systems | Retention (draft) |\n|------------|---------------|----------------------|---------|-------------------|\n| Device binding | Bech32 address, pubkey, challenge metadata | Legitimate interest / contract | `zunia-backend` | Until revoke + short audit window |\n| Push delivery | Push endpoint, device id | Consent (notification permission) | Backend + FCM/APNs/Web Push | Until unsubscribe |\n| Tx history cache | Address, tx hashes, summaries | Legitimate interest (service) | `zunia-indexer` | Cap per wallet; TTL TBD |\n| Support email | Email, message content | Consent / legitimate interest | Inbox | Ordinary business retention |\n| Crash / Sentry | Device model, stack (scrubbed) | Legitimate interest | Sentry | Per Sentry project policy |\n| Analytics | **Prefer none / privacy-preserving** | Consent if used | TBD | TBD |\n\n## Cross-border\n\nHosting regions TBD (Fly/Railway/Vercel). Document SCCs when vendors process EU data.\n\n## Data subject requests\n\nEmail dev@zunialab.com. Process access and erasure for server-side records. On-device keys are user-controlled; erasure means uninstall or wipe the device.\n"
  },
  {
   "id": "legal/app-store-encryption",
   "title": "App Store encryption declaration",
   "description": "Use when submitting iOS / macOS builds. Confirm with counsel; this is an engineering checklist, not legal advice.",
   "url": "https://docs.zunialab.com/docs/legal/app-store-encryption",
   "markdownUrl": "https://docs.zunialab.com/docs/legal/app-store-encryption.md",
   "section": "Legal (draft)",
   "optional": true,
   "headings": [
    "Typical Zunia facts (draft)",
    "References"
   ],
   "markdown": "# App Store encryption declaration checklist\n\nUse when submitting iOS / macOS builds. **Confirm with counsel**; this is an engineering checklist, not legal advice.\n\n## Typical Zunia facts (draft)\n\n- [ ] App uses encryption for HTTPS / TLS to backends and RPCs\n- [ ] App uses standard OS crypto (Keychain / Secure Enclave / Keystore) for key material at rest\n- [ ] App implements wallet cryptography (signing) using approved algorithms (e.g. secp256k1) in `zunia-core`\n- [ ] App implements the documented `zunia.connect.v2` pairing crypto (X25519, HKDF-SHA256, ChaCha20-Poly1305) so QR sessions are end-to-end encrypted\n- [ ] App does **not** implement undocumented proprietary encryption beyond TLS, OS key stores, wallet signing and that pairing protocol\n- [ ] Export compliance / EAR questions answered consistently with Apple's \"encryption\" questionnaire\n- [ ] If only exempt encryption (HTTPS + OS crypto), document whether annual self-classification / EAR exemption applies in your jurisdiction\n- [ ] Privacy nutrition labels match [Privacy Policy draft](https://docs.zunialab.com/docs/legal/privacy)\n- [ ] No seed phrases transmitted off device\n\n## References\n\n- Apple: Export compliance / encryption documentation in App Store Connect\n- Internal: [key management](https://github.com/Zunia-Lab/zunia-infra/blob/main/docs/security/key-management.md) (when published)\n"
  },
  {
   "id": "adr/overview",
   "title": "Architecture decisions",
   "description": "Product and security ADRs for Zunia.",
   "url": "https://docs.zunialab.com/docs/adr/overview",
   "markdownUrl": "https://docs.zunialab.com/docs/adr/overview.md",
   "section": "Architecture",
   "optional": true,
   "headings": [
    "Indexer",
    "Infra"
   ],
   "markdown": "# Architecture decisions\n\nProduct and security ADRs for Zunia.\n\n| ADR | Topic | Status |\n|-----|-------|--------|\n| [Core 0003](https://github.com/Zunia-Lab/zunia-core/blob/main/docs/adr/0003-custody-model.md) | Custody model | **Accepted: self-custody only** |\n| [Indexer 0001](https://github.com/Zunia-Lab/zunia-indexer/blob/main/docs/adr/0001-user-scoped-tx-history.md) | Tx history | **Accepted: user-scoped LCD** (first 5, cap 50) |\n| [Indexer 0002](https://github.com/Zunia-Lab/zunia-indexer/blob/main/docs/adr/0002-realtime-subscribed-txs.md) | Realtime detect/queue | **Accepted** |\n| [Indexer 0003](https://github.com/Zunia-Lab/zunia-indexer/blob/main/docs/adr/0003-realtime-worker-host.md) | Worker host | **Accepted: container (Fly/Railway/Render), not Vercel serverless** |\n| [Core 0002](https://github.com/Zunia-Lab/zunia-core/blob/main/docs/adr/0002-wallet-kernel-language.md) | Kernel language | **Accepted: one Rust kernel** (WASM in the extension, FFI on mobile) |\n| [Core 0004](https://github.com/Zunia-Lab/zunia-core/blob/main/docs/adr/0004-cosmos-client-libs.md) | Cosmos client libs | **Accepted: signing and encoding in Rust**, CosmJS for network I/O only |\n| [Core 0005](https://github.com/Zunia-Lab/zunia-core/blob/main/docs/adr/0005-polyrepo-package-flow.md) | Polyrepo package flow | **Accepted: published, versioned packages** |\n\n## Indexer\n\nHono + Zod + Postgres (`DATABASE_URL`) for durable history; realtime WS on always-on worker via `zunia-infra`. See [zunia-indexer](https://github.com/Zunia-Lab/zunia-indexer).\n\n## Infra\n\n[`zunia-infra`](https://github.com/Zunia-Lab/zunia-infra) holds production hosting: nginx and systemd on a Hetzner host, behind Cloudflare, plus shared DNS for `api.`, `link.` and `status.`. Its Pulumi code is still a stub. The chain registry keeps its own Pulumi stack.\n"
  },
  {
   "id": "developers/reproducible-builds",
   "title": "Reproducible builds",
   "description": "Zunia aims for bit-for-bit verifiable wallet artifacts so users and auditors can rebuild what they install.",
   "url": "https://docs.zunialab.com/docs/developers/reproducible-builds",
   "markdownUrl": "https://docs.zunialab.com/docs/developers/reproducible-builds.md",
   "section": "Developers",
   "optional": true,
   "headings": [
    "Kernel (`zunia-core`)",
    "Browser extension",
    "What is not claimed yet"
   ],
   "markdown": "# Reproducible builds\n\nZunia aims for **bit-for-bit verifiable** wallet artifacts so users and auditors can rebuild what they install.\n\n## Kernel (`zunia-core`)\n\nThe signing kernel ships npm WASM, native mobile libraries, and Dart FFI bindings. Release process, tags, checksums, and consumer update PRs are documented in:\n\n**[zunia-core/RELEASING.md](https://github.com/Zunia-Lab/zunia-core/blob/main/RELEASING.md)**\n\nVerify release artifacts against `SHA256SUMS` (and GPG signatures when the release key is published).\n\n## Browser extension\n\nExtension packaging must be rebuildable from a signed git tag. The draft rebuild steps are in [zunia-extension/docs/reproducible-builds.md](https://github.com/Zunia-Lab/zunia-extension/blob/main/docs/reproducible-builds.md). No external rebuild has passed them yet, so the Chrome Web Store build is **not** claimed reproducible. Store rewrapping means a bit-identical Chrome Web Store package is not a goal yet either.\n\n## What is not claimed yet\n\n- App Store / Play Store binaries are **not** asserted reproducible until the release pipeline and public instructions are complete.\n- Do not treat draft docs or local `pnpm build` output as an audited release.\n"
  }
 ]
}
