@fonte-is/fonte
v0.5.9
Published
Open-source SDK for tracking launch links to paying customers with Fonte Cloud
Downloads
1,670
Readme
Fonte
Fonte helps technical founders track launch links to paying customers.
Install the open-source SDK before launch, create Fonte links for each channel, preserve the source through checkout, and let Fonte Cloud connect source, checkout attempts, Stripe revenue, and diagnostics.
npm i @fonte-is/fonte
npx fonte init --framework next --mode production
npx fonte campaign link --url https://example.com/launch --link-token fl_8xK2pQ9v --utm-source x --utm-medium social --utm-campaign launch
npx fonte doctor install --config fonte.checkout.json --json
npx fonte doctor checkout --config fonte.checkout.json --json@fonte-is/fonte is the public Next.js integration kernel for Fonte Cloud.
The package captures consented browser evidence, creates server-authoritative conversion attempts, carries pointer ids through monetization paths, and posts verified revenue events from your authority system. It does not compute attribution in the browser, store provider secrets, or replace Fonte Cloud.
The SDK exposes surface adapters that emit valid Fonte events; it does not own
global event semantics. The public ABI lives in
docs/public-contracts.md. Fonte Cloud remains the
authority for source-token meaning, event admission, revenue ledger semantics,
match policy, diagnostics, and exports.
Docs
- Source to revenue
- Launch links
- Stripe Checkout
- Verified Checkout Installation
- Agent install
- Agent handoff
- Public contracts
Install
npm i @fonte-is/fonte
npx fonte initFor a Next.js App Router project, fonte init creates:
fonte.config.tssrc/fonte/browser.tsorfonte/browser.tssrc/fonte/server.tsorfonte/server.tssrc/components/fonte-capture.tsxorcomponents/fonte-capture.tsxsrc/app/api/fonte/collect/route.tsorapp/api/fonte/collect/route.ts
Render ConsentScript in your root layout head and the generated capture
component in your root layout body. Then create a conversion attempt before
redirecting to the external monetization path, attach pointer metadata where
the provider permits it, and post verified revenue events from your authority
system.
Public Imports
Use runtime-specific subpaths. Do not use a single mega import; the import path is part of the browser/server boundary.
import { createCapture } from "@fonte-is/fonte/browser";
import { createClient } from "@fonte-is/fonte/server";
import { ConsentScript, Fonte } from "@fonte-is/fonte/next";
import { collect } from "@fonte-is/fonte/next/server";
import { stripe } from "@fonte-is/fonte/checkout";The root package exports shared types only.
Browser
"use client";
import { createCapture } from "@fonte-is/fonte/browser";
export const fonte = createCapture({
storage: "example",
collect: "/api/fonte/collect",
maxAgeDays: 90,
});fonte.page();
const context = fonte.context();
const clientAttemptId = fonte.clientAttemptId();
const conversionEventId = fonte.conversionEventId();With no stored consent choice and no explicit defaults, Fonte starts at
unknown, shows the default banner through <Fonte capture={fonte} />, and
does not collect measurement or ad evidence until the visitor chooses. Consent
choices are persisted in first-party browser storage and included in every
collected scope.
The browser can reserve a clientAttemptId and conversionEventId while it
captures touch context. Fonte Cloud remains the authority for
conversionAttemptId; when supplied to attempt(...), conversionEventId is
the caller-reserved export/dedupe id Fonte accepts or deduplicates.
When measurement consent is not granted, the browser helper does not create
Fonte device or journey identifiers and does not persist attribution context.
When adStorage consent is not granted, the browser helper does not emit
page_view or source_touch collection requests, because Fonte Cloud's V0
touch admission requires ad-storage consent before it accepts browser evidence.
Browser capture defaults to source-touch capture: a source-changing arrival
with a Fonte link token, UTM evidence, platform click ids, external
referrer evidence, or the first direct landing is captured. Internal route
changes do not become new source touches unless they carry fresh source
evidence. Use capturePolicy.mode: "all" only when an app intentionally wants
to send every route scope.
Captured browser URLs are canonicalized before collection. The SDK keeps the
origin, path, Fonte token, UTM parameters when measurement consent is granted,
and platform click ids when ad-storage consent is granted. Other query
parameters and fragments are dropped so page-view capture does not forward
magic-link, reset, email, or other application-specific URL data.
The canonical Fonte source pointer is a compact link token:
https://www.example.com/promo?fonte=fl_8xK2pQ9vFonte Cloud owns what fl_8xK2pQ9v means for a tenant/environment/campaign.
UTMs are optional compatibility labels for other tools, not Fonte's source of
truth. For human and agent workflows, create the cloud campaign/link record
first and use the returned trackedUrl or compatibilityUrl:
npx fonte cloud configure \
--activation-base-url https://api.fonte.is \
--project-id 00000000-0000-0000-0000-000000000001 \
--environment production
FONTE_TENANT_API_KEY=fonte_live_... npx fonte campaign link \
--url https://www.example.com/promo \
--campaign-id camp_example_launch_20260525 \
--name "Launch Promo" \
--channel-type owned \
--channel email \
--source example_learning \
--source-platform email \
--link-id link_hero_cta \
--content hero_cta \
--utm-campaign launch_promo_20260525When a control-plane campaign link already exists and you only need a local
compiler, pass the returned linkToken and add UTMs only when needed:
npx fonte campaign link \
--url https://www.example.com/promo \
--link-token fl_8xK2pQ9v \
--utm-source newsletter \
--utm-medium email \
--utm-campaign launch_promo_20260525 \
--utm-content hero_ctaThe same deterministic helper is available in code:
import { campaign } from "@fonte-is/fonte/campaign";
const trackedUrl = campaign.url("https://www.example.com/promo", {
linkToken: "fl_8xK2pQ9v",
utmSource: "newsletter",
utmMedium: "email",
utmCampaign: "launch_promo_20260525",
utmContent: "hero_cta",
});Next Consent and Capture
import { ConsentScript } from "@fonte-is/fonte/next";
import { FonteCapture } from "@/components/fonte-capture";
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<ConsentScript />
</head>
<body>
{children}
<FonteCapture />
</body>
</html>
);
}The generated FonteCapture component wraps the browser capture primitive:
"use client";
import { Fonte } from "@fonte-is/fonte/next";
import { fonte } from "../fonte/browser";
export function FonteCapture() {
return <Fonte capture={fonte} />;
}ConsentScript sets default-denied Google Consent Mode fields before platform
tags can run. The Fonte component shows the default consent UI when consent is
unknown, persists accept/reject/manage choices, updates Google consent state,
and runs route-aware page capture.
Server
import "server-only";
import { createClient } from "@fonte-is/fonte/server";
export const fonte = createClient({
baseUrl: process.env.FONTE_API_BASE_URL!,
tenantId: (process.env.FONTE_TENANT_ID ?? process.env.FONTE_PROJECT_ID)!,
tenantApiKey:
(process.env.FONTE_TENANT_API_KEY ?? process.env.FONTE_SECRET_KEY)!,
environment:
process.env.FONTE_ENVIRONMENT === "sandbox" ? "sandbox" : "production",
timeoutMs: Number(process.env.FONTE_API_TIMEOUT_MS ?? 3000),
});const attempt = await fonte.attempt({
idempotencyKey: `checkout:${clientAttemptId}`,
occurredAt: new Date().toISOString(),
source: "example.com",
clientAttemptId,
conversionEventId,
attributionContext: context,
surface: "checkout",
product: "example-product",
offer: "full-pay",
raw: { context },
});The configured client exposes the primitives:
attempt(...)touch(...)identify(...)revenue(...)
Next Collect Route
import { collect } from "@fonte-is/fonte/next/server";
import { fonte } from "@/fonte/server";
export async function POST(request: Request) {
const origin = request.headers.get("origin");
if (!origin) {
return Response.json({ ok: false, error: "missing_origin" }, { status: 403 });
}
const body = await collect.parse(request).catch(() => null);
if (!body) {
return Response.json({ ok: false, error: "invalid_touch" }, { status: 400 });
}
const scope = collect.acceptScope(body.scope, {
requestUrl: request.url,
siteUrl: process.env.NEXT_PUBLIC_SITE_URL,
userAgent: request.headers.get("user-agent"),
});
if (!scope) {
return Response.json({ ok: false, error: "invalid_touch" }, { status: 400 });
}
const result = await fonte.touch({
idempotencyKey: `touch:${body.eventId}`,
occurredAt: new Date().toISOString(),
source: "example.com",
eventId: body.eventId,
event: body.eventType,
raw: {
...(body.eventType === "source_touch"
? { sourceTouch: collect.classifySourceTouch(scope) }
: {}),
scope,
},
touch: collect.toTouch(scope, body.journeyId),
});
return Response.json({ ok: true, result });
}Your app owns origin policy, rate limits, funnel semantics, and checkout
authority. The browser helper emits page_view for page renders only after
measurement and ad-storage consent are both granted, and emits source_touch
separately when attribution evidence is present. Fonte owns
parsing helpers, cloud writes, and conversion lineage. A browser collect POST
is not proof that Fonte Cloud accepted the touch; the control plane still
applies server-side consent and evidence admission before writing rows.
The Next collect helpers apply the same URL canonicalization server-side before
the app writes browser evidence.
Checkout
When accepting browser attribution context in checkout routes, let the SDK normalize it while applying your route's origin policy:
import { collect } from "@fonte-is/fonte/next/server";
const context = collect.acceptContext(body.attributionContext, {
requestUrl: request.url,
siteUrl: process.env.NEXT_PUBLIC_SITE_URL,
userAgent: request.headers.get("user-agent"),
});Do not maintain an app-local allowlist of Fonte context keys. The SDK owns the
source-context contract; app routes should only decide whether the request is
trusted for their own origin and funnel. Pass the accepted context directly to
fonte.attempt(...); the server client flattens the current URL, Fonte link
token, consent state, client user agent, and browser ids into the control-plane
attempt ABI.
When measurement consent is denied or unknown, accepted browser attribution
context may still carry a Fonte link token as checkout source context, but it
does not create or forward browser journey/device ids, browser-captured client
user agent, persisted attribution, or source touches.
An explicit server-side clientUserAgent follows the control-plane attempt ABI:
it is forwarded unless the attempt's measurement consent is denied.
Meta browser identifiers (fbclid, fbc, and fbp) follow the stricter
attempt ABI: the server client forwards explicit or accepted-context values only
when the effective attempt consent has both measurement and adStorage
granted. The control plane remains authoritative and may still drop identifiers
that do not satisfy its runtime evidence rules.
If an app passes explicit journeyId or deviceId together with denied or
unknown measurement consent, the server client withholds those browser ids.
import { stripe } from "@fonte-is/fonte/checkout";
const metadata = stripe.metadata({
clientAttemptId,
conversionAttemptId: attempt.conversionAttemptId,
conversionEventId: attempt.conversionEventId,
journeyId: context?.current?.fonte_journey_id,
surface: "checkout",
});stripe.metadata(...) returns plain metadata. It has no Stripe dependency and
does not store attribution blobs in Stripe. Stripe should carry pointer ids
only; Fonte Cloud stores evidence custody.
For new Stripe Checkout routes, prefer stripe.checkout.sessions.create(...).
It validates the provider session template, creates the Fonte attempt, attaches
pointer metadata, and creates the Stripe Checkout Session in one call. The
default signalMode is best_effort_revenue_safe, so checkout revenue is not
blocked by a Fonte binding failure; use strict_signal_required for tests or
controlled flows, and disabled only for routes with no Fonte purchase-signal
expectation.
Use stripe.checkoutSessionParams(...) when creating Stripe Checkout Sessions
so the same pointer reaches the Stripe objects that may later produce webhook
revenue:
const checkoutParams = stripe.checkoutSessionParams({
mode: "payment",
pointer: metadata,
base: {
success_url: successUrl,
cancel_url: cancelUrl,
line_items: lineItems,
},
});For payment-mode Checkout, the helper attaches pointer metadata to:
metadatapayment_intent_data.metadatainvoice_creation.invoice_data.metadata, only when invoice creation already exists orincludeInvoiceCreationMetadata: trueis setclient_reference_id, defaulting toconversion_event_id
The direct Fonte Cloud Stripe webhook path currently ingests invoice-centered
revenue. For one-time payment Checkout, enable Stripe invoice creation or post
verified revenue from your own Stripe webhook through the server revenue
helper; pointer metadata on a PaymentIntent alone is not an invoice-paid revenue
event.
For subscription or payment-plan Checkout, use mode: "subscription":
const checkoutParams = stripe.checkoutSessionParams({
mode: "subscription",
pointer: metadata,
base: {
success_url: successUrl,
cancel_url: cancelUrl,
line_items: lineItems,
},
});The helper returns a plain object for Stripe's checkout.sessions.create(...).
It preserves existing non-Fonte metadata and overwrites Fonte pointer keys with
the current pointer values so stale conversion_event_id values cannot survive
in nested Stripe objects.
For a full Next.js payment-plan route, see
examples/nextjs-stripe-payment-plan.
Before live traffic, run the static export-readiness fixture:
npx fonte export-readiness \
--fixture node_modules/@fonte-is/fonte/fixtures/simple_checkout_payment_plan_lineage.json \
--jsonThe probe does not query Fonte Cloud or dispatch conversions. It checks the
fixture for pointer continuity, including Stripe invoice/webhook-visible
metadata, and applies the V1 Meta/Google eligibility reason vocabulary:
consent_denied, missing_meta_event_source_url,
missing_meta_client_user_agent, missing_meta_match_keys, and
missing_google_click_or_user_data.
For customer-app install verification, add a generic checkout ownership config and run the checkout doctor:
{
"sourceRoutes": ["/", "/blog/example"],
"checkoutScenarios": [
{
"name": "premium_one_time_offer",
"startUrl": "/offer/register",
"expectedValue": {
"basis": "fixed_amount",
"amountMinor": 150000,
"currency": "USD"
}
},
{
"name": "subscription_monthly_plan",
"startUrl": "/subscribe",
"expectedValue": {
"basis": "initial_payment",
"amountMinor": 4900,
"currency": "USD"
}
}
],
"browserPurchase": {
"policy": "absent_or_paired"
}
}npx fonte doctor install --config fonte.checkout.json --json
npx fonte doctor checkout --config fonte.checkout.json --jsonfonte doctor install reduces the local SDK/checkout checks and optional Fonte
Cloud evidence-quality read model into generic first-party integration
checkpoints: source/session capture wiring, consent evidence, checkout binding,
pointer preservation, browser-batch ingress, projection custody, and blocked
evidence frontier. Checkpoint statuses are present, missing, blocked, or
not_yet_verified.
fonte doctor checkout is deliberately local and read-only. It verifies the app
has an installable checkout ownership contract, detects static browser-side
Purchase code that must be absent or paired with Fonte's conversionEventId,
and checks that Stripe Checkout creation follows the public Fonte pointer
pattern. Neither doctor creates test payments, inspects external tag-manager
rules, calls providers, or claims platform delivery.
expectedValue should use the object form in new configs. fixed_amount and
initial_payment require amountMinor in currency minor units, so $1,500.00
is 150000. provider_price_lookup is accepted for dynamic checks that verify a
configured Stripe price instead of a hardcoded amount; the verifier resolves
priceRef from the local environment and checks the Checkout Session line items
include that Stripe price:
{
"name": "provider_price_subscription",
"startUrl": "/subscribe",
"expectedValue": {
"basis": "provider_price_lookup",
"provider": "stripe",
"priceRef": "STRIPE_SUBSCRIPTION_PRICE_ID",
"currency": "USD"
}
}Numeric expectedValue remains supported as a legacy shorthand for fixed major
units, but public install guides should prefer the object form.
For dynamic local verification, add dynamicVerification to each scenario. The
CLI opens the local app, calls the configured checkout-start route with generated
test ids, verifies the Stripe test Checkout Session metadata, and observes
browser-side Purchase events on the local success path when one is configured:
{
"name": "premium_one_time_offer",
"startUrl": "/offer/register",
"expectedValue": {
"basis": "fixed_amount",
"amountMinor": 150000,
"currency": "USD"
},
"dynamicVerification": {
"checkoutStart": {
"path": "/api/checkout/start",
"body": {
"clientAttemptId": "{{clientAttemptId}}",
"conversionEventId": "{{conversionEventId}}",
"email": "{{smokeEmail}}"
},
"checkoutUrlJsonPath": "checkoutUrl"
},
"successPath": "/checkout/success"
}
}Run it only against a local app with test payment credentials and app-side
measurement writes disabled or smoke-gated. The command uses a local
Chrome/Chromium executable; pass --chrome-path or set CHROME_PATH if it
cannot locate one automatically:
STRIPE_SECRET_KEY=sk_test_... \
npx fonte verify checkout \
--config fonte.checkout.json \
--base-url http://localhost:3000 \
--jsonfonte verify checkout refuses non-loopback app URLs and non-test Stripe keys.
It adds x-fonte-checkout-ownership-smoke: v0 to the checkout-start request,
but the customer app must enforce any write-suppression behavior for that header.
The command does not complete payment, navigate to Stripe Checkout, query Fonte
Cloud, query Meta Events Manager, or prove external tag-manager behavior. Browser
Purchase observations are summarized in the report; raw browser event payloads
are not echoed.
Use browserPurchase.policy = "absent" when the app must not contain repo-local
browser Purchase code. Use "paired" only when the app intentionally owns a
browser Purchase path that must share Fonte's conversionEventId/value with
the server event; if the local static scan cannot find that path, the doctor
warns instead of treating the policy as satisfied. Dynamic verification is
stricter: "paired" must observe a matching browser Purchase during the local
flow. absent_or_paired is the default V0 posture for apps that have not yet
chosen between those two designs.
Vocabulary
context: browser attribution contextscope: one route/page/touch scope inside a contextattempt: a Fonte Cloud conversion attempt that may later become revenueconversionAttemptId: Fonte Cloud canonical attempt idconversionEventId: platform export and browser/server dedupe lineage idrevenue: verified authority event from Stripe, CRM, or another source
