DomainValuation.site
The New Generation of Domain Valuation
Including Blockchain Domain Valution
Plan for The ANALYTICS.1 Dashboard
What the dashboard should be
The dashboard should be positioned as the control plane for an analytics namespace — not just “site analytics.”
The winning framing is:
-
ANALYTICS.1 = the Web3-native analytics endpoint
-
Web2 twin (e.g., analytics.website / analytics1.site) = onboarding + HTTPS access
-
The dashboard is where operators, partners, and subdomain tenants monitor performance, campaigns, and revenue across your ecosystem.
Use of the dashboard
This should be a multi-tenant analytics console that supports:
-
The WEB-3 network
Track each TLD site / landing page / partner page as a “tenant,” including:
-
traffic and conversion funnels
-
partner campaign performance
-
lead submissions / signups
-
lease inquiries / purchases
-
affiliate/referrer attribution
-
On-chain + off-chain in one view (dual stack)
Combine:
-
Web events (page views, CTA clicks, form submits)
-
Campaign metadata (UTM, referrer, partner)
-
On-chain signals (mints, leases, payments, wallet connects) if applicable
-
A “generic endpoint” model
What this offers:
-
tenant.analytics.1as a namespace concept (even if it’s routed today) -
api.analytics.1for ingest/query endpoints (Web2-hosted but branded as the analytics root) -
a “verified analytics” directory later (optional)
As well as providing Analytics for the Web-3 Network and deriving an income from this, the operator of Analytics1.Site and Analytics.1 will also be able to independently operate the sites and derive income externally.
Phased build
Phase 1 (MVP, 1–2 weeks):
-
Dashboard UI
-
Event tracking snippet (1 JS file)
-
Ingest endpoint (POST events)
-
Basic reports: KPIs, time series, top pages, top referrers, campaign performance, conversions
Phase 2 (Real product, 2–6 weeks):
-
Multi-tenant RBAC (admin / partner / viewer)
-
Alerts (spikes, drops, bot traffic)
-
Export + API keys per tenant
-
Consent + privacy settings (GDPR-friendly)
Phase 3 (Web3-native differentiator):
-
Wallet-based access (Sign-In With Ethereum)
-
On-chain indexing integration (The Graph / RPC indexing)
-
Proof/attestation of campaign metrics if you want an auditable layer
Architecture (simple and scalable)
-
Frontend: React + Tailwind + Recharts (fast to ship, good visuals)
-
Backend API: Node/Express (ingest + query)
-
Storage: start with SQLite/Postgres; scale to ClickHouse later
-
Event model:
tenant_id,event,url,referrer,utm_*,wallet,timestamp
Production-ready starter code (Frontend + Backend)
Below is a working starter that can be can run in minutes:
-
A modern analytics dashboard UI (multi-tenant selector, date range, KPIs, charts, table)
-
A simple backend with:
-
POST /api/events(ingest) -
GET /api/metrics(dashboard query) -
(Real sources can be wired later - (PostHog, Plausible, GA4 export, ClickHouse, Subgraphs, etc.).
-
1) Backend (Node + Express) — server.js
// server.js
// Minimal analytics ingest + metrics API (in-memory store for MVP).
// Replace the in-memory array with SQLite/Postgres/ClickHouse as you scale.
import express from "express";
import cors from "cors";
const app = express();
app.use(cors());
app.use(express.json({ limit: "1mb" }));
/**
* Event schema (MVP):
* {
* tenantId: "analytics.1" | "scotland.web-3" | ...
* type: "pageview" | "click" | "signup" | "purchase" | ...
* url: "/pricing"
* referrer: "https://example.com"
* utm: { source, medium, campaign, content, term }
* wallet: "0x..." (optional)
* ts: number (ms epoch)
* }
*/
const events = [];
// Helpers
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
const parseMs = (v, fallback) => {
const n = Number(v);
return Number.isFinite(n) ? n : fallback;
};
const dayKey = (ms) => {
const d = new Date(ms);
const yyyy = d.getUTCFullYear();
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
const dd = String(d.getUTCDate()).padStart(2, "0");
return `${yyyy}-${mm}-${dd}`;
};
app.post("/api/events", (req, res) => {
const body = req.body || {};
const now = Date.now();
const tenantId = String(body.tenantId || "analytics.1").slice(0, 80);
const type = String(body.type || "pageview").slice(0, 40);
const url = String(body.url || "/").slice(0, 300);
const referrer = String(body.referrer || "").slice(0, 300);
const utm = body.utm && typeof body.utm === "object" ? body.utm : {};
const wallet = body.wallet ? String(body.wallet).slice(0, 120) : null;
const ts = clamp(parseMs(body.ts, now), now - 1000 * 60 * 60 * 24 * 365, now + 1000 * 60);
events.push({
tenantId,
type,
url,
referrer,
utm: {
source: String(utm.source || ""),
medium: String(utm.medium || ""),
campaign: String(utm.campaign || ""),
content: String(utm.content || ""),
term: String(utm.term || ""),
},
wallet,
ts,
});
// Keep memory bounded for MVP
if (events.length > 200000) events.splice(0, events.length - 200000);
res.json({ ok: true });
});
// Compute metrics for a tenant and date range.
app.get("/api/metrics", (req, res) => {
const now = Date.now();
const tenantId = String(req.query.tenantId || "analytics.1");
const from = parseMs(req.query.from, now - 1000 * 60 * 60 * 24 * 30);
const to = parseMs(req.query.to, now);
const filtered = events.filter(
(e) => e.tenantId === tenantId && e.ts >= from && e.ts <= to
);
// KPIs
const pageviews = filtered.filter((e) => e.type === "pageview").length;
const clicks = filtered.filter((e) => e.type === "click").length;
const signups = filtered.filter((e) => e.type === "signup").length;
const purchases = filtered.filter((e) => e.type === "purchase").length;
const conversionRate = pageviews > 0 ? (signups / pageviews) : 0;
// Time series (daily)
const byDay = new Map();
for (const e of filtered) {
const k = dayKey(e.ts);
if (!byDay.has(k)) byDay.set(k, { day: k, pageviews: 0, signups: 0, purchases: 0 });
const row = byDay.get(k);
if (e.type === "pageview") row.pageviews += 1;
if (e.type === "signup") row.signups += 1;
if (e.type === "purchase") row.purchases += 1;
}
const series = Array.from(byDay.values()).sort((a, b) => a.day.localeCompare(b.day));
// Top pages
const pageCounts = new Map();
for (const e of filtered) {
if (e.type !== "pageview") continue;
pageCounts.set(e.url, (pageCounts.get(e.url) || 0) + 1);
}
const topPages = Array.from(pageCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([url, count]) => ({ url, count }));
// Top referrers
const refCounts = new Map();
for (const e of filtered) {
if (e.type !== "pageview") continue;
const key = e.referrer || "(direct)";
refCounts.set(key, (refCounts.get(key) || 0) + 1);
}
const topReferrers = Array.from(refCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([referrer, count]) => ({ referrer, count }));
// Campaign performance
const campCounts = new Map();
for (const e of filtered) {
const c = (e.utm?.campaign || "").trim() || "(none)";
campCounts.set(c, (campCounts.get(c) || 0) + 1);
}
const campaigns = Array.from(campCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([campaign, events]) => ({ campaign, events }));
res.json({
ok: true,
tenantId,
range: { from, to },
kpis: { pageviews, clicks, signups, purchases, conversionRate },
series,
topPages,
topReferrers,
campaigns,
});
});
const port = process.env.PORT || 8787;
app.listen(port, () => console.log(`API running on http://localhost:${port}`));
Run backend
mkdir analytics-1 && cd analytics-1
npm init -y
npm i express cors
# enable ESM:
node -e "const p=require('./package.json'); p.type='module'; require('fs').writeFileSync('package.json', JSON.stringify(p,null,2));"
node server.js
2) Frontend (React + Tailwind + Recharts) — App.jsx
This is a complete dashboard UI. It expects the backend above on http://localhost:8787.
// App.jsx
import React, { useEffect, useMemo, useState } from "react";
import {
LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
BarChart, Bar, Legend
} from "recharts";
// Simple helpers
const fmtInt = (n) => new Intl.NumberFormat().format(n ?? 0);
const fmtPct = (n) => `${((n ?? 0) * 100).toFixed(1)}%`;
const daysAgoMs = (d) => Date.now() - d * 24 * 60 * 60 * 1000;
function Card({ title, value, subtitle }) {
return (
<div className="rounded-2xl border bg-white p-4 shadow-sm">
<div className="text-sm text-gray-500">1testAnalytics2025</div>
<div className="mt-1 text-2xl font-semibold text-gray-900">{value}</div>
{subtitle ? <div className="mt-1 text-xs text-gray-500">{subtitle}</div> : null}
</div>
);
}
function Table({ title, columns, rows, rowKey }) {
return (
<div className="rounded-2xl border bg-white p-4 shadow-sm">
<div className="mb-3 text-sm font-semibold text-gray-900">1testAnalytics2025</div>
<div className="overflow-auto">
<table className="min-w-full text-sm">
<thead className="text-left text-gray-500">
<tr>
{columns.map((c) => (
<th key={c.key} className="border-b px-2 py-2 font-medium">
{c.label}
</th>
))}
</tr>
</thead>
<tbody className="text-gray-800">
{rows.map((r) => (
<tr key={rowKey(r)} className="hover:bg-gray-50">
{columns.map((c) => (
<td key={c.key} className="border-b px-2 py-2">
{c.render ? c.render(r) : r[c.key]}
</td>
))}
</tr>
))}
{rows.length === 0 ? (
<tr>
<td className="px-2 py-6 text-gray-500" colSpan={columns.length}>
No data in this range.
</td>
</tr>
) : null}
</tbody>
</table>
</div>
</div>
);
}
export default function App() {
const [tenantId, setTenantId] = useState("analytics.1");
const [from, setFrom] = useState(daysAgoMs(30));
const [to, setTo] = useState(Date.now());
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState("");
const apiBase = "http://localhost:8787";
const tenants = useMemo(
() => [
"analytics.1",
"web-3.scotland",
"web-3.gbr",
"avgas.1",
"lettings.1",
"zipcode.1",
],
[]
);
async function load() {
setLoading(true);
setError("");
try {
const url = new URL(`${apiBase}/api/metrics`);
url.searchParams.set("tenantId", tenantId);
url.searchParams.set("from", String(from));
url.searchParams.set("to", String(to));
const res = await fetch(url.toString());
const json = await res.json();
if (!json.ok) throw new Error("API error");
setData(json);
} catch (e) {
setError(e?.message || "Failed to load");
} finally {
setLoading(false);
}
}
useEffect(() => {
load();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tenantId]);
// Quick local event simulator so you can see the dashboard populate
async function simulateTraffic() {
const pages = ["/", "/pricing", "/partners", "/docs", "/contact"];
const refs = ["(direct)", "https://x.com", "https://google.com", "https://reddit.com", "https://example.com"];
const campaigns = ["(none)", "winter-launch", "partner-a", "affiliate-42"];
const now = Date.now();
const n = 250 + Math.floor(Math.random() * 400);
for (let i = 0; i < n; i++) {
const ts = now - Math.floor(Math.random() * 1000 * 60 * 60 * 24 * 21);
const roll = Math.random();
let type = "pageview";
if (roll > 0.82) type = "click";
if (roll > 0.92) type = "signup";
if (roll > 0.985) type = "purchase";
const payload = {
tenantId,
type,
url: pages[Math.floor(Math.random() * pages.length)],
referrer: refs[Math.floor(Math.random() * refs.length)] === "(direct)" ? "" : refs[Math.floor(Math.random() * refs.length)],
utm: { campaign: campaigns[Math.floor(Math.random() * campaigns.length)] },
ts,
};
await fetch(`${apiBase}/api/events`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
}
await load();
}
const k = data?.kpis || {};
const series = data?.series || [];
return (
<div className="min-h-screen bg-gray-50">
<header className="border-b bg-white">
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-4">
<div>
<div className="text-lg font-semibold text-gray-900">ANALYTICS.1 Dashboard</div>
<div className="text-xs text-gray-500">Multi-tenant analytics control plane (MVP)</div>
</div>
<div className="flex items-center gap-2">
<select
className="rounded-xl border px-3 py-2 text-sm"
value={tenantId}
onChange={(e) => setTenantId(e.target.value)}
>
{tenants.map((t) => (
<option key={t} value={t}>{t}</option>
))}
</select>
<button
className="rounded-xl border px-3 py-2 text-sm hover:bg-gray-50"
onClick={load}
disabled={loading}
>
{loading ? "Loading…" : "Refresh"}
</button>
<button
className="rounded-xl bg-gray-900 px-3 py-2 text-sm text-white hover:opacity-90"
onClick={simulateTraffic}
disabled={loading}
title="Generates sample events so you can see the charts update."
>
Simulate traffic
</button>
</div>
</div>
</header>
<main className="mx-auto max-w-6xl px-4 py-6">
{/* Date controls */}
<div className="mb-6 flex flex-wrap items-center gap-2">
<div className="text-sm text-gray-600">Date range:</div>
<button
className="rounded-xl border px-3 py-2 text-sm hover:bg-gray-50"
onClick={() => {
setFrom(daysAgoMs(7));
setTo(Date.now());
}}
>
Last 7 days
</button>
<button
className="rounded-xl border px-3 py-2 text-sm hover:bg-gray-50"
onClick={() => {
setFrom(daysAgoMs(30));
setTo(Date.now());
}}
>
Last 30 days
</button>
<button
className="rounded-xl border px-3 py-2 text-sm hover:bg-gray-50"
onClick={() => {
setFrom(daysAgoMs(90));
setTo(Date.now());
}}
>
Last 90 days
</button>
<button
className="rounded-xl bg-white border px-3 py-2 text-sm hover:bg-gray-50"
onClick={load}
>
Apply
</button>
{error ? <div className="ml-2 text-sm text-red-600">{error}</div> : null}
</div>
{/* KPIs */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card title="Pageviews" value={fmtInt(k.pageviews)} subtitle={tenantId} />
<Card title="Clicks" value={fmtInt(k.clicks)} subtitle="CTA + interactions" />
<Card title="Signups" value={fmtInt(k.signups)} subtitle="Form / onboarding" />
<Card title="Signup rate" value={fmtPct(k.conversionRate)} subtitle="Signups / pageviews" />
</div>
{/* Charts */}
<div className="mt-6 grid grid-cols-1 gap-4 lg:grid-cols-2">
<div className="rounded-2xl border bg-white p-4 shadow-sm">
<div className="mb-3 text-sm font-semibold text-gray-900">Traffic over time</div>
<div className="h-72">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={series}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="day" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="pageviews" strokeWidth={2} dot={false} />
<Line type="monotone" dataKey="signups" strokeWidth={2} dot={false} />
<Line type="monotone" dataKey="purchases" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
<div className="rounded-2xl border bg-white p-4 shadow-sm">
<div className="mb-3 text-sm font-semibold text-gray-900">Daily signups</div>
<div className="h-72">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={series}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="day" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} />
<Tooltip />
<Bar dataKey="signups" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
{/* Tables */}
<div className="mt-6 grid grid-cols-1 gap-4 lg:grid-cols-2">
<Table
title="Top pages"
columns={[
{ key: "url", label: "URL" },
{ key: "count", label: "Pageviews", render: (r) => fmtInt(r.count) },
]}
rows={data?.topPages || []}
rowKey={(r) => r.url}
/>
<Table
title="Top referrers"
columns={[
{ key: "referrer", label: "Referrer" },
{ key: "count", label: "Pageviews", render: (r) => fmtInt(r.count) },
]}
rows={data?.topReferrers || []}
rowKey={(r) => r.referrer}
/>
</div>
<div className="mt-6">
<Table
title="Campaigns (by events)"
columns={[
{ key: "campaign", label: "Campaign" },
{ key: "events", label: "Events", render: (r) => fmtInt(r.events) },
]}
rows={data?.campaigns || []}
rowKey={(r) => r.campaign}
/>
</div>
<footer className="mt-10 text-xs text-gray-500">
Tip: Once deployed, keep the dashboard on your Web2 twin for HTTPS trust, and treat ANALYTICS.1 as the namespace and API brand.
</footer>
</main>
</div>
);
}
Create the frontend project
Use Vite + React + Tailwind + Recharts:
# in a new terminal
npm create vite@latest analytics-dashboard -- --template react
cd analytics-dashboard
npm i recharts
npm i
npm run dev
Add Tailwind
npm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Update tailwind.config.js:
export default {
content: ["./index.html", "./src/**/*.{js,jsx}"],
theme: { extend: {} },
plugins: [],
};
Update src/index.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
Replace src/App.jsx with the code above.
How to deploy the Dashboard under ANALYTICS.1)
Recommended deployment pattern (dual-stack):
-
Host dashboard at your Web2 twin:
https://analytics.website/dashboard(or similar) -
Brand the product as ANALYTICS.1 Dashboard
-
Provide gateway access links like:
https://hns.to/analytics.1that redirect users to the Web2 dashboard
Who it serves best:
-
Web-3 Network admin: oversees all WEB-3 Network tenants
-
Partners: view their own tenant dashboards
-
Future buyers: immediately see the “namespace-as-a-network” story
What to build next to increase value quickly:
-
Add tenant onboarding + API keys
-
Add role-based access (admin/partner/viewer)
-
Add conversion funnels and a simple alert system
-
Add on-chain event ingestion (wallet connect, mints, lease payments)
Next.js + Postgres + ClickHouse (later) is the best balance of speed to ship, enterprise credibility, and scaling headroom.
Frontend + API
-
Next.js (App Router) + TypeScript
-
Tailwind for UI
-
Recharts (or Tremor) for charts
-
Host on Vercel (fast deploys, global CDN)
Auth / Tenancy
-
Clerk or Auth.js (NextAuth)
-
Clerk is fastest if you want polished org/role management.
-
Auth.js is great if you want “own it” and keep costs down.
-
-
Use “Organizations” / “Teams” for multi-tenant access
Primary data store
-
Postgres (Supabase or Neon)
-
Supabase if you want admin UI, edge functions, and storage built-in.
-
Neon if you want pure Postgres + excellent scaling.
-
Event ingestion
-
Start with: Next.js API route (or a tiny Node service) writing to Postgres
-
After you get traction: move event storage to ClickHouse for cheap, fast analytics queries.
Why this stack wins
-
You can ship a serious MVP in days.
-
Postgres is perfect for tenants/users/billing/config.
-
ClickHouse is the industry standard for high-volume analytics events (PostHog uses it).
-
This stack is easy to sell to partners/investors.
What lives where
-
Postgres: users, tenants, sites, API keys, feature flags, billing, alerts
-
ClickHouse: raw events + aggregated rollups
Alternative 1 (fastest MVP): Next.js + Supabase only
To launch very quickly and keep infra simple:
-
Next.js + Supabase (Postgres + Auth + Storage + Edge Functions)
-
Store events in Postgres first, roll up daily aggregates
-
When the dataset grows, add ClickHouse later
Pros
-
One vendor does most things
-
Very fast to implement multi-tenancy
-
Great dashboards/admin convenience
Cons
-
Postgres is not ideal for massive event volumes long-term (but fine early)
Alternative 2 (privacy-first + low overhead): Cloudflare stack - “web3-native vibes” + global edge + low cost:
-
Cloudflare Pages (frontend)
-
Cloudflare Workers (ingest API at the edge)
-
D1 (SQLite-ish) for config
-
R2 for raw event blobs
-
Optional: send events to ClickHouse later
Pros
-
Very cheap at scale for ingestion
-
Extremely fast globally
-
Great for “endpoint” positioning (analytics.1 feels like edge infra)
Cons
-
More engineering complexity than Next.js+Supabase
-
D1 isn’t as flexible as Postgres for complex analytics queries
Customization for the WEB-3 Network)
The Web-3 network will comprise many TLD sites, leasing, partner pages, payments, analytics). So it needs:
-
multi-tenant roles
-
partner dashboards
-
billing + metering
-
long-term scale
Appropriate stack
Next.js + TypeScript + Tailwind + Auth (Clerk/Auth.js) + Postgres (Supabase/Neon) + ClickHouse later
The shortest path:
-
Next.js + Supabase + Recharts
-
Add ClickHouse only when event volume demands it
Practical “starter architecture”
Week 1 MVP
-
Next.js dashboard (tenant selector, KPIs, charts, referrers, campaigns)
-
Event snippet →
/api/collect -
Postgres tables:
-
tenants, sites, api_keys
-
events (or daily_rollups)
-
Week 2
-
Organizations/roles
-
Alerts (traffic spike/drop)
-
Funnel tracking
-
Export CSV
Week 3+
-
ClickHouse event store
-
Real-time dashboards
-
On-chain event connectors (wallet connect, mints, lease payments)
. Commercial Terms (Proposed)
Term Length
-
Initial term: 3 years
-
Renewal: automatic by mutual agreement
Revenue Share (Network-Derived Revenue)
Applies to revenue generated from Web-3 Network tenants and partners:
-
70% Operator
-
30% Web-3 Network
This reflects:
-
operator responsibility for build, hosting, compliance, and support,
-
Web-3 Network contribution of domain, distribution, and ecosystem.
Independent Revenue
-
100% retained by the Operator
-
Applies to all non-network clients, products, and services
Minimum Operator Obligations
The operator must:
-
Maintain uptime ≥ 99.5% (rolling 30 days)
-
Provide dashboards and reporting for all core network TLDs
-
Support onboarding for new network tenants
-
Maintain GDPR-compliant data handling
-
Provide quarterly performance summaries to the Web-3 Network
Network Obligations
The Web-3 Network will:
-
Designate ANALYTICS.1 as the official analytics endpoint
-
Promote analytics usage across its ecosystem
-
Avoid operating a competing analytics platform
-
Provide reasonable notice of major network changes
Exit / Termination
Either party may terminate if:
-
material breach remains uncured after notice,
-
prolonged service failure occurs,
-
insolvency or abandonment is evident.
On termination:
-
operator retains independent analytics business,
-
network retains domain stewardship and data continuity rights.
