skills-ws
Version:
87 agent skills for AI coding assistants — marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.
116 lines (97 loc) • 5.14 kB
Markdown
- 9. Building Dashboards
- Architecture
- Dune API Integration (robust: terminal states, HTTP errors, pagination, rate limits)
- Dashboard Data Patterns
## 9. Building Dashboards
### Architecture
```
Data sources → ETL/Indexer → Database → API → Frontend
│ │
├── Dune API (SQL queries, scheduled) ├── Next.js + Chart.js/Recharts
├── Etherscan V2 API (lookups) ├── TanStack Query for caching
├── The Graph (GraphQL queries/polling) └── Tailwind for styling
└── RPC nodes / indexer (custom indexing)
```
> Don't hammer Dune's execute endpoint from the browser on every page load — executions cost credits and take seconds-to-minutes. Pattern: **schedule** the query on Dune (or run it server-side on a cron), then have the frontend read **cached latest results** through your own API route. Keep the Dune key server-side only.
### Dune API Integration (robust: terminal states, HTTP errors, pagination, rate limits)
The common failing pattern only loops while `PENDING`/`EXECUTING` and then blindly reads `result.result.rows` — so a `FAILED`/`CANCELLED`/`EXPIRED` execution either throws an opaque `undefined` error or silently returns nothing, and large result sets are truncated at the first page. Handle all terminal states, check HTTP status, respect 429s, and page via `next_uri`.
```typescript
const DUNE_API_KEY = process.env.DUNE_API_KEY!;
const DUNE = 'https://api.dune.com/api/v1';
const H = { 'X-Dune-API-Key': DUNE_API_KEY };
const TERMINAL_OK = 'QUERY_STATE_COMPLETED';
const TERMINAL_FAIL = new Set([
'QUERY_STATE_FAILED', 'QUERY_STATE_CANCELLED', 'QUERY_STATE_EXPIRED',
]);
async function duneFetch(url: string, init?: RequestInit, attempt = 0): Promise<any> {
const res = await fetch(url, init);
if (res.status === 429) { // rate limited → backoff + retry
if (attempt >= 5) throw new Error('Dune: rate limited, retries exhausted');
const retryAfter = Number(res.headers.get('retry-after')) || 2 ** attempt;
await new Promise(r => setTimeout(r, retryAfter * 1000));
return duneFetch(url, init, attempt + 1);
}
if (!res.ok) throw new Error(`Dune HTTP ${res.status}: ${await res.text()}`);
return res.json();
}
// Trigger a fresh execution and wait for a terminal state
async function executeDuneQuery(queryId: number, params?: Record<string, unknown>) {
const { execution_id } = await duneFetch(`${DUNE}/query/${queryId}/execute`, {
method: 'POST',
headers: { ...H, 'Content-Type': 'application/json' },
body: JSON.stringify(params ? { query_parameters: params } : {}),
});
// Poll status (cheap) — not the full results endpoint — until terminal
for (;;) {
await new Promise(r => setTimeout(r, 2000));
const { state } = await duneFetch(`${DUNE}/execution/${execution_id}/status`, { headers: H });
if (state === TERMINAL_OK) break;
if (TERMINAL_FAIL.has(state)) throw new Error(`Dune execution ${execution_id} ended in ${state}`);
// else PENDING / EXECUTING → keep polling
}
return fetchAllRows(`${DUNE}/execution/${execution_id}/results?limit=1000`);
}
// Page through every result via next_uri (or next_offset)
async function fetchAllRows(firstUrl: string) {
const rows: any[] = [];
let url: string | undefined = firstUrl;
while (url) {
const page = await duneFetch(url, { headers: H });
rows.push(...(page.result?.rows ?? []));
// Dune returns an absolute next_uri when more pages exist
url = page.next_uri;
}
return rows;
}
// Cached latest results (no credits, no re-execution) — preferred for dashboards
async function getLatestResults(queryId: number) {
return fetchAllRows(`${DUNE}/query/${queryId}/results?limit=1000`);
}
```
> Parameter typing: Dune `query_parameters` are typed (`text`, `number`, `date` as `YYYY-MM-DD HH:mm:ss`, `enum`). Pass them as the correct JS type or the execution fails validation. There's also a one-shot `POST /query/{id}/execute` + `GET /execution/{id}/results` flow shown here, plus a higher-level "run query" convenience endpoint — check current limits and credit costs at https://docs.dune.com.
```typescript
// React component with TanStack Query
import { useQuery } from '@tanstack/react-query';
function TVLChart({ queryId }: { queryId: number }) {
const { data, isLoading } = useQuery({
queryKey: ['tvl', queryId],
queryFn: () => getLatestResults(queryId),
staleTime: 5 * 60 * 1000, // 5 min cache
refetchInterval: 10 * 60 * 1000, // refresh every 10 min
});
if (isLoading) return <Skeleton />;
return (
<ResponsiveContainer width="100%" height={400}>
<AreaChart data={data}>
<XAxis dataKey="day" />
<YAxis tickFormatter={(v) => `$${(v / 1e6).toFixed(0)}M`} />
<Tooltip formatter={(v: number) => `$${v.toLocaleString()}`} />
<Area type="monotone" dataKey="tvl" stroke="#8884d8" fill="#8884d8" fillOpacity={0.3} />
</AreaChart>
</ResponsiveContainer>
);
}
```
---