Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/app/products/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { claimAgeLabel, viewClaims } from "@/lib/provider-claims";
import { distinctBenchCount } from "@/lib/providers";
import type { Metadata } from "next";
import { loadSitemapBlob } from "@/lib/sitemap-blob";
Expand Down Expand Up @@ -1061,6 +1062,58 @@ export default async function ProviderPage({
</section>
)}

{(() => {
// Properties this site has no instrument for: insurance,
// confidentiality, whether users are shielded from questionable
// liquidity. Real, and worth a reader knowing, but not measured. They
// get their own section with the source and the date attached, never
// a column next to the measured ones, and the heading names the
// speaker. See lib/provider-claims.ts.
const claims = viewClaims(reg?.claims);
if (claims.length === 0) return null;
return (
<section className="mt-10">
<h2 className="text-[11px] font-medium uppercase tracking-[0.18em] text-ink-muted">
Stated by {p.name}, not measured here
</h2>
<div className="mt-2 h-px bg-rule" />
<p className="mt-3 max-w-2xl text-sm text-ink-muted leading-relaxed">
These are the provider&rsquo;s own statements about properties no
benchmark on this site measures. We checked that each one appears
at the source on the date given; we did not verify that it holds.
</p>
<dl className="mt-5 flex flex-col gap-4">
{claims.map((c) => (
<div key={c.label} className="border-l-2 border-rule pl-4">
<dt className="text-[11px] font-medium uppercase tracking-[0.16em] text-ink-muted">
{c.label}
</dt>
<dd className="mt-1 text-base text-ink-soft leading-relaxed">
{c.value}
</dd>
<dd className="mt-1 text-xs text-ink-faint">
<a
className="lnk underline underline-offset-2 hover:text-ink"
href={withUtm(c.source)}
rel="noopener nofollow"
>
source
</a>
{" · "}
<time dateTime={c.verifiedAt}>{claimAgeLabel(c)}</time>
{c.stale && (
<span className="ml-2 text-warn">
not re-checked recently
</span>
)}
</dd>
</div>
))}
</dl>
</section>
);
})()}

{(() => {
// Most recent lastRunAt across every appearance is the truest
// "last measured" signal a Google freshness ranker, an AI citation
Expand Down
40 changes: 37 additions & 3 deletions src/data/provider-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,43 @@
* factual, no marketing adjectives. Twitter handle without the URL.
*/

/**
* A statement the provider makes about itself that no harness can measure:
* swap insurance, confidentiality, whether users are shielded from
* questionable liquidity. Aurora asked for exactly these, and they are real
* properties, but they are not measurements.
*
* Three fields a measurement does not need, all required: where we read it,
* when we last looked, and the provider's own words for it. The product page
* renders all three under a heading that says the provider is speaking, and
* flags a claim nobody has re-checked in six months. See lib/provider-claims.ts.
*
* A claim never enters a benchmark, never ranks anything, and never moves a
* leader. If a property can be probed, probe it and publish the measurement
* instead; this is only for the ones that cannot.
*/
export type ProviderClaim = {
/** What the claim is about. "Swap insurance", "Confidentiality". */
label: string;
/** The provider's position, in plain words, no superlatives. */
value: string;
/** https URL where this was read. An unsourced claim is a rumour. */
source: string;
/** YYYY-MM-DD the source was last checked by a human. */
verifiedAt: string;
};

export type ProviderRegistryEntry = {
url: string;
description: string;
twitter?: string;

// Optional rich content surfaced on the product detail page when present.
// Every field is independently optional; the page renders only the
// sections that have data, so partial enrichment is safe.
// Optional enrichment. NOTE: of the seven fields below, none is currently
// read by any page. They were added for a product-page section that was
// never built, and the comment here used to claim they were "surfaced on
// the product detail page", which is not true. Either wire them or drop
// them; do not add an eighth expecting it to render. `claims` below IS
// rendered, and its test fails the build if a claim is malformed.
longDescription?: string;
chains?: string[];
features?: string[];
Expand All @@ -33,6 +62,11 @@ export type ProviderRegistryEntry = {
* Sub-products keep their own bench rankings — this is editorial
* cross-linking, not data merging. */
parent?: string;

/** Provider statements we cannot measure, each with a source and a date.
* Rendered in their own section, never mixed with measured columns.
* See lib/provider-claims.ts for the rules and the staleness window. */
claims?: ProviderClaim[];
};

export const PROVIDER_REGISTRY: Record<string, ProviderRegistryEntry> = {
Expand Down
90 changes: 90 additions & 0 deletions src/lib/provider-claims.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, expect, test } from "bun:test";
import { PROVIDER_REGISTRY, type ProviderClaim } from "@/data/provider-registry";
import { claimAgeDays, claimAgeLabel, claimProblems, viewClaims } from "./provider-claims";

const ok: ProviderClaim = {
label: "Swap insurance",
value: "Swaps are covered by a third-party policy up to the notional amount.",
source: "https://example.com/terms",
verifiedAt: "2026-10-01",
};

describe("a claim must be sourced and dated to be published", () => {
test("a well-formed claim has no problems", () => {
expect(claimProblems("x", ok)).toEqual([]);
});

test("an unsourced claim is a rumour", () => {
expect(claimProblems("x", { ...ok, source: "" })[0]).toContain("https URL");
expect(claimProblems("x", { ...ok, source: "http://example.com" })[0]).toContain("https URL");
});

test("a claim without a readable date cannot be aged", () => {
expect(claimProblems("x", { ...ok, verifiedAt: "last tuesday" })[0]).toContain("YYYY-MM-DD");
});

test("a future verification date is a typo, not a fresh check", () => {
expect(claimProblems("x", { ...ok, verifiedAt: "2099-01-01" })[0]).toContain("future");
});

test("a superlative in a claim would be marketing we are laundering", () => {
// The measured columns are where a provider gets to be fastest. This site
// exists because marketing pages quote "real-time" without a number.
const p = claimProblems("x", { ...ok, value: "The fastest settlement in the market." });
expect(p[0]).toContain("superlative");
});
});

describe("age and staleness", () => {
const now = new Date("2026-10-02T12:00:00Z");

test("counts whole days from the verification date", () => {
expect(claimAgeDays("2026-10-02", now)).toBe(0);
expect(claimAgeDays("2026-09-02", now)).toBe(30);
});

test("a future date yields no age rather than a negative one", () => {
expect(claimAgeDays("2027-01-01", now)).toBeNull();
});

test("past six months a claim says so instead of ageing quietly", () => {
const fresh = viewClaims([{ ...ok, verifiedAt: "2026-09-20" }], now)[0];
const old = viewClaims([{ ...ok, verifiedAt: "2026-01-01" }], now)[0];
expect(fresh.stale).toBe(false);
expect(old.stale).toBe(true);
});

test("stale claims sort last, so the section opens on what we stand behind", () => {
const views = viewClaims(
[
{ ...ok, label: "Old", verifiedAt: "2025-01-01" },
{ ...ok, label: "New", verifiedAt: "2026-09-30" },
],
now,
);
expect(views.map((v) => v.label)).toEqual(["New", "Old"]);
});

test("the age label reads like a person wrote it", () => {
expect(claimAgeLabel(viewClaims([{ ...ok, verifiedAt: "2026-10-02" }], now)[0])).toBe("checked today");
expect(claimAgeLabel(viewClaims([{ ...ok, verifiedAt: "2026-10-01" }], now)[0])).toBe("checked yesterday");
expect(claimAgeLabel(viewClaims([{ ...ok, verifiedAt: "2026-09-22" }], now)[0])).toBe("checked 10 days ago");
expect(claimAgeLabel(viewClaims([{ ...ok, verifiedAt: "2026-04-02" }], now)[0])).toContain("months ago");
});

test("no claims renders nothing rather than an empty section", () => {
expect(viewClaims(undefined)).toEqual([]);
expect(viewClaims([])).toEqual([]);
});
});

describe("the registry itself", () => {
test("every claim in the registry is publishable", () => {
// This is the gate: a malformed claim fails the build rather than
// reaching a page unsourced or undated.
const problems = Object.entries(PROVIDER_REGISTRY).flatMap(([slug, entry]) =>
(entry.claims ?? []).flatMap((c) => claimProblems(slug, c)),
);
expect(problems).toEqual([]);
});
});
89 changes: 89 additions & 0 deletions src/lib/provider-claims.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { ProviderClaim } from "@/data/provider-registry";

/**
* Statements a provider makes about itself, which no harness can measure.
*
* Aurora asked us to cover things this site has no instrument for: whether a
* swap is insured, whether it is confidential, whether users are shielded
* from questionable liquidity. Those are real properties and they matter to a
* reader, but they are not measurements and must never be printed as if they
* were. This site exists because marketing pages quote "real-time" without a
* number; publishing a vendor's claim unqualified would make us the thing we
* were built against.
*
* So a claim carries three things a measurement does not need: where we read
* it, when we last looked, and the fact that it is the provider speaking. The
* page renders all three, and a claim nobody has re-checked in a long time
* says so rather than ageing silently into something that reads current.
*
* What a claim is NOT: a benchmark value, a ranking input, or anything that
* can move a leader. Nothing here touches the ledger.
*/

/** A claim goes stale at six months. Long enough that re-checking is not
* busywork, short enough that a pricing page or a terms change is unlikely to
* have passed unnoticed for two of them. */
export const CLAIM_STALE_AFTER_DAYS = 183;

export type ClaimView = ProviderClaim & {
/** Whole days since it was last verified; null when the date will not parse. */
ageDays: number | null;
/** Past CLAIM_STALE_AFTER_DAYS, so the page says so instead of ageing quietly. */
stale: boolean;
};

/** Days between an ISO date and now, or null when the date is unusable. */
export function claimAgeDays(verifiedAt: string, now = new Date()): number | null {
const d = new Date(`${verifiedAt}T00:00:00Z`);
if (Number.isNaN(d.getTime())) return null;
const ms = now.getTime() - d.getTime();
if (ms < 0) return null; // a future date is a typo, not a fresh check
return Math.floor(ms / 86_400_000);
}

/** Claims in the order a reader should meet them: anything stale last, so the
* section opens on what we actually stand behind. */
export function viewClaims(claims: ProviderClaim[] | undefined, now = new Date()): ClaimView[] {
if (!claims || claims.length === 0) return [];
return claims
.map((c) => {
const ageDays = claimAgeDays(c.verifiedAt, now);
return { ...c, ageDays, stale: ageDays == null || ageDays > CLAIM_STALE_AFTER_DAYS };
})
.sort((a, b) => Number(a.stale) - Number(b.stale) || a.label.localeCompare(b.label));
}

/** "checked 12 days ago", for the line under each claim. A claim whose date
* will not parse says that plainly rather than printing a wrong age. */
export function claimAgeLabel(c: ClaimView): string {
if (c.ageDays == null) return "verification date unreadable";
if (c.ageDays === 0) return "checked today";
if (c.ageDays === 1) return "checked yesterday";
if (c.ageDays < 60) return `checked ${c.ageDays} days ago`;
const months = Math.round(c.ageDays / 30.4);
return `checked ${months} months ago`;
}

/** Problems that should stop a claim reaching a page. Returned rather than
* thrown so a test can assert the whole registry at once. */
export function claimProblems(slug: string, c: ProviderClaim): string[] {
const out: string[] = [];
const where = `${slug} claim "${c.label}"`;
if (!c.label.trim()) out.push(`${where}: empty label`);
if (!c.value.trim()) out.push(`${where}: empty value`);
if (!/^https:\/\//.test(c.source)) {
// An unsourced claim is a rumour, and http is not a source we can stand on.
out.push(`${where}: source must be an https URL, got ${c.source || "(none)"}`);
}
if (!/^\d{4}-\d{2}-\d{2}$/.test(c.verifiedAt)) {
out.push(`${where}: verifiedAt must be YYYY-MM-DD, got ${c.verifiedAt || "(none)"}`);
} else if (claimAgeDays(c.verifiedAt) == null) {
out.push(`${where}: verifiedAt is in the future or unparseable (${c.verifiedAt})`);
}
if (/\b(fastest|best|cheapest|leading|#1|number one)\b/i.test(c.value)) {
// A superlative in a claim is marketing we would be laundering. The
// measured columns are where a provider gets to be fastest.
out.push(`${where}: value carries a superlative; quote the substance, not the boast`);
}
return out;
}
Loading