Skip to main content

Overview

While live contract reads tell you what’s happening right now, analytics provide the context of your application’s growth and health. Integrating Dune Analytics allows you to provide users with historical insights, network-wide benchmarks, and aggregate metrics.

Initia App

Handles wallet connections, onchain transactions, and the primary UI.

Secure Backend

A lightweight proxy that securely manages your private Dune API key.

Dune Analytics

The SQL engine that processes historical data and serves API results.

Prerequisites

Ensure you have the following before starting:
  • Dune API Key: Obtained from your Dune settings (select API keys from the sidebar).
  • Saved Query IDs: The numeric IDs for the queries you want to display. Open Dune Queries to create or inspect them.
  • Node.js Environment: To run the backend proxy and frontend application.

Step 1: Backend Implementation

Your backend acts as a secure proxy, ensuring your DUNE_API_KEY is never exposed to the frontend.
Pattern: The backend proxy pattern is language-agnostic. The examples here use Node.js and Express, but the same security model applies in Python, Go, Rust, or any server-side stack.

1. Setup and Configuration

Create an api/ directory, a src/ folder inside it, and install the required dependencies: The backend example uses express for routing, cors for origin control, and dotenv for environment variables.
mkdir api && cd api
mkdir src
npm init -y
npm install express cors dotenv
npm pkg set type="module"
Update api/package.json with these scripts:
api/package.json
{
  "type": "module",
  "scripts": {
    "dev": "node --watch src/server.js",
    "start": "node src/server.js"
  }
}
Create api/.env and store your secrets in it:
api/.env
PORT=4000
DUNE_API_KEY=YOUR_DUNE_API_KEY
# Use the numeric IDs from your Dune dashboard.
DUNE_ALLOWED_QUERY_IDS=1234567,2345678,3456789
FRONTEND_ORIGIN=http://localhost:5173

2. Proxy Server

This Express server validates requests and proxies them to Dune. It keeps DUNE_API_KEY server-side and only forwards allowed query IDs.
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 }))
// Kept in place so the server is ready for JSON routes later.
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) => {
  if (!duneApiKey) {
    res.status(500).json({
      error: 'DUNE_API_KEY is not configured on the backend.',
    })
    return
  }

  const { queryId } = req.params
  const limit = String(req.query.limit ?? '8')

  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 http://localhost:${port}`)
})

Step 2: Frontend Implementation

The frontend connects to your backend proxy, not to Dune directly.

1. Configuration

Switch to your frontend directory before setting environment variables:
cd ../frontend
Add your backend’s base URL and query IDs to your public environment variables (e.g., .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

2. Fetch Helper

Use this helper to abstract the backend API call:
frontend/src/lib/dune.ts
const DUNE_API_BASE_URL =
  import.meta.env.VITE_DUNE_API_BASE_URL ?? 'http://localhost:4000/api'

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()
}
Limit: The backend proxy supports ?limit= on the results endpoint, and the frontend helper exposes it as the optional limit argument.

Step 3: Mapping Multiple Queries

Use stable Application Keys to map your UI components to specific Dune Query IDs. This allows you to update queries on the backend without changing frontend code.
frontend/src/lib/dune.ts
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
}

3. Usage Example

After defining DUNE_QUERY_MAP, a React component can use the helper like this:
frontend/src/components/Analytics.tsx
import { useEffect, useState } from 'react'
import { DUNE_QUERY_MAP, fetchLatestDuneResult } from '../lib/dune'

export function Analytics() {
  const [data, setData] = useState([])
  const [error, setError] = useState('')

  useEffect(() => {
    fetchLatestDuneResult(DUNE_QUERY_MAP.overview.queryId)
      .then((result) => setData(result.result.rows))
      .catch((err) => setError(err.message))
  }, [])

  if (error) return <p>{error}</p>

  return <pre>{JSON.stringify(data, null, 2)}</pre>
}

Step 4: Local Development

1

Set Query IDs

Note the numeric IDs for your saved queries in the Dune dashboard.
2

Start Backend

Run your proxy server from the api/ directory with npm run dev.
3

Start Frontend

Run the frontend app from the frontend/ directory with npm run dev and verify VITE_DUNE_API_BASE_URL points to the backend.
4

Verify Backend

Confirm your proxy is live by visiting http://localhost:4000/api/health in your browser or running: bash curl http://localhost:4000/api/health
5

Verify Results

Launch your app and refresh a query to confirm the frontend renders data and the backend logs successful requests.

Troubleshooting and Security Tips

  • API Key Security: Never expose DUNE_API_KEY in your frontend. It must remain server-side.
  • Source of Truth: Dune is an analytics layer, not your contract’s source of truth. Keep critical validation and state transitions 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, not necessarily a fresh one.
  • Maintainability: Use Application Keys (like overview) in your UI components instead of hardcoding numeric IDs.

Advanced: Saving Preferences Onchain (Optional)

For personalized experiences, you can store a user’s selected analytics view on the Initia rollup.
struct SavedView {
    uint256 id;
    address owner;
    string viewKey; // e.g., "overview"
    uint64 createdAt;
    bool archived;
}
Performance: Store only the preference (the view key) onchain. The actual analytics data should always be fetched from Dune via your backend.

Resources

Common Dune Example Tables

These are example tables frequently used for Initia analytics:
TableRecommended Use
initia.transactionsNetwork activity and gas trends.
initia.bridge_transfersAsset flow and bridge route analytics.
initia.tx_messagesDetailed message-type activity.

Choosing the Right Tool

FeatureInitia IndexerDune Analytics
Data ScopeReal-time app state.Historical trends & aggregates.
Query StyleSpecific accounts/events.Complex SQL across millions of rows.
Primary UseCore app logic.Dashboards & public reports.

Next Steps

Backend Caching

Reduce API consumption by caching results on your server.

Rich Dashboards

Combine results from multiple Dune views into a single UI.