Skip to Content
Addon DevelopmentExample: Crypto Tracker

Example: Crypto Tracker addon (complete code)

Below is a self-contained teaching addon. Replace mock prices with a signed server route or allowlisted HTTP API before production.

⚠️ Warning: This sample uses Math.random() for demo prices — never ship that to users.

manifest.json

{ "schema": 1, "id": "com.example.addon.crypto-tracker", "name": "Crypto Tracker", "version": "1.0.0", "author": "Example Dev", "license": "MIT", "description": "Demo BTC/ETH price tile with voice query.", "permissions": ["network.fetch", "storage.local", "voice.register", "voice.tts"], "network": { "allowlist": ["https://api.coingecko.com"] }, "widget": { "entry": "dist/widget.js", "defaultRegion": "top-right", "minWidth": 2, "minHeight": 2 }, "voice": { "intents": ["crypto.price"] } }

src/CryptoWidget.tsx

import { useEffect, useState } from "react"; type Props = { settings: { symbols?: string[] }; updateSettings: (p: Record<string, unknown>) => Promise<void>; theme: "dark" | "light"; http: { get: (url: string) => Promise<{ json: () => Promise<unknown> }> }; piper?: { speak: (t: string) => Promise<void> }; voice?: { registerIntent: ( id: string, def: unknown, handler: (slots: { symbol?: string }) => Promise<{ speak: string }>, ) => void; }; }; export default function CryptoWidget(props: Props) { const { settings, theme, http, voice, piper } = props; const symbols = settings.symbols?.length ? settings.symbols : ["bitcoin", "ethereum"]; const [prices, setPrices] = useState<Record<string, number>>({}); useEffect(() => { voice?.registerIntent( "crypto.price", { slots: { symbol: { type: "string" } }, examples: ["Hey Piper, what's Bitcoin at?"], }, async ({ symbol }) => { const sym = (symbol || "bitcoin").toLowerCase(); const px = prices[sym] ?? (await fetchPrice(http, sym)); const line = `${sym} is about ${px.toFixed(0)} AUD (demo)`; await piper?.speak(line); return { speak: line }; }, ); }, [voice, piper, prices, http]); useEffect(() => { let alive = true; (async () => { const next: Record<string, number> = {}; for (const id of symbols) { next[id] = await fetchPrice(http, id); } if (alive) setPrices(next); })(); const t = setInterval(async () => { const next: Record<string, number> = {}; for (const id of symbols) { next[id] = await fetchPrice(http, id); } if (alive) setPrices(next); }, 60_000); return () => { alive = false; clearInterval(t); }; }, [http, symbols.join("|")]); return ( <div data-theme={theme} style={{ padding: 12, borderRadius: 12, background: theme === "dark" ? "#0f172a" : "#f8fafc", color: theme === "dark" ? "#e2e8f0" : "#0f172a", fontFamily: "system-ui, sans-serif", }} > <div style={{ fontWeight: 700, marginBottom: 8 }}>Crypto (demo)</div> {symbols.map((s) => ( <div key={s} style={{ display: "flex", justifyContent: "space-between" }}> <span>{s}</span> <span>{prices[s] != null ? `${prices[s].toFixed(2)} AUD` : "…"}</span> </div> ))} <button type="button" style={{ marginTop: 8 }} onClick={() => props.updateSettings({ symbols: ["bitcoin"] })} > Show BTC only </button> </div> ); } async function fetchPrice( http: Props["http"], id: string, ): Promise<number> { try { const url = `https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent( id, )}&vs_currencies=aud`; const res = await http.get(url); const body = (await res.json()) as Record<string, { aud: number }>; return body[id]?.aud ?? 0; } catch { return 42000 + Math.random() * 5000; } }

src/entry.tsx (bundle iife / esm entry)

import CryptoWidget from "./CryptoWidget"; export default CryptoWidget;

Build notes

  1. Bundle with esbuild / vite as iife named CryptoWidget or default export matching core loader.
  2. Output to dist/widget.js referenced by manifest.

Troubleshooting

429 from CoinGecko
Add caching server-side or reduce refresh interval.

Last updated on

Was this helpful?