diff --git a/developers/developer-guides/guides/dune-integration.mdx b/developers/developer-guides/guides/dune-integration.mdx
new file mode 100644
index 00000000..683456d5
--- /dev/null
+++ b/developers/developer-guides/guides/dune-integration.mdx
@@ -0,0 +1,609 @@
+---
+title: Integrating Dune Analytics
+description:
+ Build analytics-driven Initia applications by bridging onchain state with
+ Dune's historical data.
+---
+
+## Overview
+
+Dune is an onchain analytics platform for exploring historical blockchain data,
+building SQL queries, and turning the results into dashboards or API-backed
+application views. While live contract reads tell you what's happening _right
+now_, Dune adds the historical context that helps you explain growth, usage, and
+network health.
+
+### The End-State
+
+By the end of this guide, you will have a functional **Onchain Saved Views**
+implementation where:
+
+1. **Users** save their preferred analytics view (e.g., Bridge Volume) to a
+ smart contract on an Initia rollup.
+2. **The Frontend** reads that onchain preference and fetches matching
+ historical data from Dune via a secure proxy.
+3. **The Developer** maintains full control over API keys and allowed queries
+ without exposing secrets to the browser.
+
+
+
+ The UI built with InterwovenKit for wallet connection and onchain actions.
+
+
+ A VM-specific contract or module that stores user-specific analytics
+ preferences.
+
+
+ A lightweight proxy that manages your `DUNE_API_KEY` and filters requests.
+
+
+ The SQL engine that processes historical data and serves indexed results.
+
+
+
+### Choosing the Right Tool
+
+Use the Initia Indexer for live app state and Dune for historical analysis,
+reporting, and dashboards.
+
+| Feature | [Initia Indexer](/api-reference/rollup-indexer/introduction) | Dune Analytics |
+| --------------- | ------------------------------------------------------------ | ----------------------------------- |
+| **Data Scope** | Real-time app state | Historical trends and aggregates |
+| **Query Style** | Specific accounts and events | Complex SQL across millions of rows |
+| **Primary Use** | Core app logic | Dashboards and public reports |
+
+## Prerequisites
+
+- **Dune API Key:** Obtained from your
+ [Dune settings](https://dune.com/settings/profile).
+- **Saved Query IDs:** The numeric IDs for the queries you want to display in
+ your app.
+- **Node.js Environment:** To run the backend proxy and frontend application.
+- **Foundry:** To deploy the preference registry contract to your Initia rollup.
+- **Registry Contract Address:** The deployed address for your saved-view
+ contract.
+
+## Step 0: Build Your Dune Queries
+
+Before wiring anything together, create the SQL queries your app will display.
+Run these example templates in the Dune Query Editor and save each one to get a
+numeric query ID. You will plug those IDs into the backend allowlist and the
+frontend `.env` in the next steps.
+
+
+ You can also browse public examples on the [query
+ page](https://dune.com/queries) when signed in. For table-specific ideas,
+ search for `Initia` in Dune's data catalog.
+
+
+### 1. Daily Transaction Volume
+
+```sql
+SELECT
+ date_trunc('day', b.block_timestamp) AS day,
+ count(*) AS tx_count,
+ count(distinct t.fee_payer) AS unique_fee_payers
+FROM initia.transactions t
+JOIN initia.blocks b
+ ON t.chain_id = b.chain_id
+ AND t.block_height = b.block_height
+WHERE b.block_date >= CURRENT_DATE - INTERVAL '30' DAY
+GROUP BY 1
+ORDER BY 1 DESC
+```
+
+### 2. Popular Message Types
+
+```sql
+SELECT
+ message_type,
+ count(*) AS count
+FROM initia.tx_messages
+GROUP BY 1
+ORDER BY 2 DESC
+LIMIT 10
+```
+
+## Step 1: Backend Implementation
+
+Your backend acts as a secure proxy, ensuring your `DUNE_API_KEY` is never
+exposed to the frontend.
+
+
+ Never expose `DUNE_API_KEY` in client-side code. Keep the key on the backend
+ and proxy all Dune requests through your server.
+
+
+### 1. Setup and Configuration
+
+Create an `api/` directory and install the required dependencies:
+
+```bash
+mkdir api && cd api
+npm init -y
+npm install express cors dotenv
+```
+
+Create `api/.env` and store your backend configuration there:
+
+```env api/.env
+PORT=4000
+DUNE_API_KEY=YOUR_DUNE_API_KEY
+DUNE_ALLOWED_QUERY_IDS=1234567,2345678,3456789
+FRONTEND_ORIGIN=http://localhost:5173
+```
+
+Update `api/package.json` with these scripts:
+
+```json
+{
+ "type": "module",
+ "scripts": {
+ "dev": "node --watch src/server.js",
+ "start": "node src/server.js"
+ }
+}
+```
+
+### 2. Proxy Server
+
+This server validates requests and only forwards allowed query IDs to Dune.
+
+```js api/src/server.js
+import 'dotenv/config'
+import cors from 'cors'
+import express from 'express'
+
+const app = express()
+const port = Number(process.env.PORT ?? 4000)
+const duneApiKey = process.env.DUNE_API_KEY
+const frontendOrigin = process.env.FRONTEND_ORIGIN ?? 'http://localhost:5173'
+const allowedQueryIds = new Set(
+ (process.env.DUNE_ALLOWED_QUERY_IDS ?? '')
+ .split(',')
+ .map((value) => value.trim())
+ .filter(Boolean),
+)
+
+app.use(cors({ origin: frontendOrigin }))
+app.use(express.json())
+
+app.get('/api/health', (_req, res) => {
+ res.json({
+ ok: true,
+ duneConfigured: Boolean(duneApiKey),
+ allowedQueryIds: [...allowedQueryIds],
+ })
+})
+
+app.get('/api/dune/query/:queryId/results', async (req, res) => {
+ const { queryId } = req.params
+ const limit = String(req.query.limit ?? '8')
+
+ if (!duneApiKey) {
+ res
+ .status(500)
+ .json({ error: 'DUNE_API_KEY is not configured on the backend.' })
+ return
+ }
+
+ if (!/^\d+$/.test(queryId)) {
+ res.status(400).json({ error: 'Query ID must be numeric.' })
+ return
+ }
+
+ if (allowedQueryIds.size > 0 && !allowedQueryIds.has(queryId)) {
+ res.status(403).json({
+ error: `Query ${queryId} is not allowed by this backend.`,
+ })
+ return
+ }
+
+ if (!/^\d+$/.test(limit)) {
+ res.status(400).json({ error: 'limit must be numeric.' })
+ return
+ }
+
+ try {
+ const duneResponse = await fetch(
+ `https://api.dune.com/api/v1/query/${queryId}/results?limit=${limit}`,
+ {
+ headers: {
+ 'x-dune-api-key': duneApiKey,
+ },
+ },
+ )
+ const bodyText = await duneResponse.text()
+ res
+ .status(duneResponse.status)
+ .type(duneResponse.headers.get('content-type') ?? 'application/json')
+ .send(bodyText)
+ } catch (error) {
+ res.status(502).json({
+ error:
+ error instanceof Error
+ ? error.message
+ : 'Failed to fetch Dune results.',
+ })
+ }
+})
+
+app.listen(port, () => {
+ console.log(`Dune Radar API listening on port ${port}`)
+})
+```
+
+## Step 2: Onchain Registry
+
+This example uses a MiniEVM Solidity contract to store user view preferences.
+The saved-view pattern is VM-agnostic, so you can implement the same registry on
+MiniMove or MiniWasm by using the equivalent Move or CosmWasm syntax.
+
+```solidity
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.24;
+
+contract DuneRadarRegistry {
+ struct SavedView {
+ uint256 id;
+ address owner;
+ string viewKey; // e.g., "overview", "bridges"
+ uint64 createdAt;
+ bool archived;
+ }
+
+ uint256 private _nextId = 1;
+ mapping(uint256 => SavedView) private _savedViews;
+ mapping(address => uint256[]) private _ownerViews;
+
+ function createSavedView(string calldata viewKey) external returns (uint256) {
+ uint256 id = _nextId++;
+ _savedViews[id] = SavedView(id, msg.sender, viewKey, uint64(block.timestamp), false);
+ _ownerViews[msg.sender].push(id);
+ return id;
+ }
+
+ function archiveSavedView(uint256 savedViewId) external {
+ SavedView storage savedView = _savedViews[savedViewId];
+ require(savedView.owner == msg.sender, "Not owner");
+ savedView.archived = true;
+ }
+
+ function getSavedViews(address owner) external view returns (SavedView[] memory) {
+ uint256[] storage ids = _ownerViews[owner];
+ SavedView[] memory results = new SavedView[](ids.length);
+ for (uint256 i = 0; i < ids.length; i++) {
+ results[i] = _savedViews[ids[i]];
+ }
+ return results;
+ }
+}
+```
+
+Deploy the contract to your rollup with Foundry, then set
+`VITE_REGISTRY_ADDRESS` in your frontend `.env` to the address it prints.
+
+```bash
+forge create src/DuneRadarRegistry.sol:DuneRadarRegistry \
+ --rpc-url $JSON_RPC_URL \
+ --private-key $DEPLOYER_KEY \
+ --broadcast
+```
+
+## Step 3: Frontend Implementation
+
+The frontend uses **InterwovenKit** to manage onchain preferences and the proxy
+for fetching historical data.
+
+### 1. Dune Helper
+
+Map stable application keys to your numeric Dune query IDs.
+
+```ts frontend/src/lib/dune.ts
+const DUNE_API_BASE_URL =
+ import.meta.env.VITE_DUNE_API_BASE_URL ?? 'http://localhost:4000/api'
+
+export const DUNE_QUERY_MAP = {
+ overview: {
+ title: 'Network Overview',
+ description:
+ 'A high-level summary of Initia transaction activity and chain health.',
+ queryId: import.meta.env.VITE_DUNE_QUERY_ID_OVERVIEW,
+ },
+ bridges: {
+ title: 'Bridge Routes',
+ description:
+ 'Cross-chain transfer flow across Initia bridge routes and assets.',
+ queryId: import.meta.env.VITE_DUNE_QUERY_ID_BRIDGES,
+ },
+ wallets: {
+ title: 'Message Activity',
+ description:
+ 'Readable message activity showing what kinds of actions wallets are taking.',
+ queryId: import.meta.env.VITE_DUNE_QUERY_ID_WALLETS,
+ },
+}
+
+export const DUNE_QUERY_OPTIONS = Object.entries(DUNE_QUERY_MAP).map(
+ ([key, value]) => ({
+ key,
+ ...value,
+ }),
+)
+
+export function getQueryConfig(queryKey) {
+ return DUNE_QUERY_MAP[queryKey] ?? DUNE_QUERY_MAP.overview
+}
+
+export async function fetchLatestDuneResult(queryId: string, limit = 8) {
+ if (!queryId) {
+ throw new Error(
+ 'Missing Dune query ID. Update the frontend .env with your Dune query IDs.',
+ )
+ }
+
+ const response = await fetch(
+ `${DUNE_API_BASE_URL}/dune/query/${queryId}/results?limit=${limit}`,
+ )
+ if (!response.ok) {
+ const text = await response.text()
+ throw new Error(
+ text || `Dune request failed with status ${response.status}`,
+ )
+ }
+ return response.json()
+}
+```
+
+```env frontend/.env
+VITE_DUNE_API_BASE_URL=http://localhost:4000/api
+VITE_DUNE_QUERY_ID_OVERVIEW=1234567
+VITE_DUNE_QUERY_ID_BRIDGES=2345678
+VITE_DUNE_QUERY_ID_WALLETS=3456789
+VITE_APPCHAIN_ID=your-appchain-id
+VITE_REGISTRY_ADDRESS=0xYourDeployedRegistryContractAddress
+VITE_JSON_RPC_URL=http://localhost:8545
+```
+
+
+ These examples use local-development defaults so you can run the guide
+ end-to-end on your machine. Replace the URLs with your deployed frontend and
+ backend origins when you publish the app.
+
+
+### 2. Contract Helper
+
+This helper encodes the `MsgCall` write for your registry contract and reads
+saved views back through a JSON-RPC `eth_call`. Both paths reuse the same ABI.
+
+```ts frontend/src/lib/contract.ts
+import { decodeFunctionResult, encodeFunctionData } from 'viem'
+
+const CONTRACT_ADDRESS = import.meta.env.VITE_REGISTRY_ADDRESS
+const CHAIN_ID = import.meta.env.VITE_APPCHAIN_ID
+const JSON_RPC_URL = import.meta.env.VITE_JSON_RPC_URL
+
+export const REGISTRY_ABI = [
+ {
+ name: 'createSavedView',
+ type: 'function',
+ stateMutability: 'nonpayable',
+ inputs: [{ name: 'viewKey', type: 'string' }],
+ outputs: [{ name: 'savedViewId', type: 'uint256' }],
+ },
+ {
+ name: 'getSavedViews',
+ type: 'function',
+ stateMutability: 'view',
+ inputs: [{ name: 'owner', type: 'address' }],
+ outputs: [
+ {
+ type: 'tuple[]',
+ components: [
+ { name: 'id', type: 'uint256' },
+ { name: 'owner', type: 'address' },
+ { name: 'viewKey', type: 'string' },
+ { name: 'createdAt', type: 'uint64' },
+ { name: 'archived', type: 'bool' },
+ ],
+ },
+ ],
+ },
+]
+
+export function buildCreateSavedViewMessage(
+ initiaAddress: string,
+ viewKey: string,
+) {
+ const input = encodeFunctionData({
+ abi: REGISTRY_ABI,
+ functionName: 'createSavedView',
+ args: [viewKey],
+ })
+
+ return {
+ chainId: CHAIN_ID,
+ messages: [
+ {
+ typeUrl: '/minievm.evm.v1.MsgCall',
+ value: {
+ sender: initiaAddress.toLowerCase(),
+ contractAddr: CONTRACT_ADDRESS,
+ input,
+ value: '0',
+ accessList: [],
+ authList: [],
+ },
+ },
+ ],
+ }
+}
+
+export async function fetchSavedViews(hexAddress: string) {
+ if (!hexAddress || !CONTRACT_ADDRESS) return []
+
+ const data = encodeFunctionData({
+ abi: REGISTRY_ABI,
+ functionName: 'getSavedViews',
+ args: [hexAddress],
+ })
+
+ const response = await fetch(JSON_RPC_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 1,
+ method: 'eth_call',
+ params: [{ to: CONTRACT_ADDRESS, data }, 'latest'],
+ }),
+ })
+
+ const { result, error } = await response.json()
+ if (error) throw new Error(error.message)
+
+ const decoded = decodeFunctionResult({
+ abi: REGISTRY_ABI,
+ functionName: 'getSavedViews',
+ data: result,
+ })
+
+ return decoded.map((view) => ({
+ ...view,
+ id: Number(view.id),
+ createdAt: Number(view.createdAt),
+ }))
+}
+```
+
+### 3. Rendering Results
+
+Use a reusable table component to format Dune's tabular data.
+
+```tsx frontend/src/components/DuneTable.tsx
+export function DuneTable({ rows }: { rows: any[] }) {
+ if (!rows?.length) return
No data found for this view.
+
+ const columns = Object.keys(rows[0]).slice(0, 5)
+
+ return (
+
+
+
+
+ {columns.map((col) => (
+ | {col} |
+ ))}
+
+
+
+ {rows.map((row, i) => (
+
+ {columns.map((col) => (
+ | {String(row[col] ?? '—')} |
+ ))}
+
+ ))}
+
+
+
+ )
+}
+```
+
+### 4. Integration Example
+
+Use `useInterwovenKit` for the write path and `useHexAddress` for the read. The
+component loads the user's saved views on mount, lets them open any saved view
+to see the matching Dune result, and refreshes the list after a new preference
+is saved.
+
+```tsx frontend/src/App.tsx
+import { useCallback, useEffect, useState } from 'react'
+import { useHexAddress, useInterwovenKit } from '@initia/interwovenkit-react'
+import {
+ DUNE_QUERY_OPTIONS,
+ fetchLatestDuneResult,
+ getQueryConfig,
+} from './lib/dune'
+import { buildCreateSavedViewMessage, fetchSavedViews } from './lib/contract'
+import { DuneTable } from './components/DuneTable'
+
+export function Analytics() {
+ const { initiaAddress, requestTxBlock } = useInterwovenKit()
+ const hexAddress = useHexAddress()
+ const [viewKey, setViewKey] = useState(
+ DUNE_QUERY_OPTIONS[0]?.key ?? 'overview',
+ )
+ const [savedViews, setSavedViews] = useState([])
+ const [rows, setRows] = useState([])
+
+ const loadSavedViews = useCallback(async () => {
+ if (!hexAddress) return
+ setSavedViews(await fetchSavedViews(hexAddress))
+ }, [hexAddress])
+
+ useEffect(() => {
+ void loadSavedViews()
+ }, [loadSavedViews])
+
+ useEffect(() => {
+ const queryId = getQueryConfig(viewKey).queryId
+ if (!queryId) return
+ void fetchLatestDuneResult(queryId).then((res) => setRows(res.result.rows))
+ }, [viewKey])
+
+ const handleSaveView = async () => {
+ if (!initiaAddress) return
+ await requestTxBlock(buildCreateSavedViewMessage(initiaAddress, viewKey))
+ await loadSavedViews()
+ }
+
+ return (
+
+
+
+
+
+ {savedViews
+ .filter((view) => !view.archived)
+ .map((view) => (
+ -
+
+
+ ))}
+
+
+
+
+ )
+}
+```
+
+## Troubleshooting and Security Tips
+
+- **API Key Security:** **Never** expose `DUNE_API_KEY` in your frontend. It
+ must remain server-side in your proxy.
+- **Source of Truth:** Dune is an analytics layer. Keep critical app logic and
+ state validation onchain.
+- **Access Control:** Always use the `DUNE_ALLOWED_QUERY_IDS` allowlist on your
+ backend to prevent unauthorized proxying.
+- **Data Freshness:** Dune's results endpoint returns the latest saved
+ execution. For real-time state, use the
+ [Initia Indexer](/api-reference/rollup-indexer/introduction).
+
+## Common Dune Tables for Initia
+
+| Table | Recommended Use |
+| ------------------------- | -------------------------------------- |
+| `initia.transactions` | Network activity and gas trends. |
+| `initia.bridge_transfers` | Asset flow and bridge route analytics. |
+| `initia.tx_messages` | Detailed message-type activity. |
diff --git a/docs.json b/docs.json
index 25d6bf8c..9ea19fdb 100644
--- a/docs.json
+++ b/docs.json
@@ -202,7 +202,10 @@
},
{
"group": "Quick Guides",
- "pages": ["developers/developer-guides/guides/exchange-integration"]
+ "pages": [
+ "developers/developer-guides/guides/exchange-integration",
+ "developers/developer-guides/guides/dune-integration"
+ ]
},
{
"group": "VM Specific Tutorials",