@clickstreamhq/signalsis a developer preview on the 0.1.0-alpha line. Every API in this article is the real, current surface — verbatim-checkable against the package — but expect it to evolve before a stable release. Of the ClickStream packages, only@clickstreamhq/sdk(1.4.0) is stable today.
Checkout Optimization Personalization, Defined Narrowly
Most checkout optimization is one-size-fits-all: run an A/B test, ship the variant that wins on average, and accept that the "average visitor" it optimizes for doesn't exist. A returning customer who has already decided gets walked through the same cross-sell interstitial as a first-timer still comparing you against two competitors. One of them wanted a shorter path; the other needed the returns policy you just streamlined away.
Checkout optimization personalization means the checkout's shape adapts to the visitor in front of it. Not its prices — its shape: which modules render, in what order, with what emphasis. This article builds that with two fields from ClickStream Signals, under two non-negotiable rules:
- Shape, never price. Every visitor sees the same prices, totals, fees, and shipping options. A score may reorder the page; it may never change what anything costs.
- Fail open. The complete, working checkout ships in your HTML. Signals decorates it when a read succeeds — and when it doesn't, nobody notices.
Two Fields Tell You What Shape to Render
The Signals endpoint (/v1/signals/:visitorId, roughly 50–150 ms per read) returns an 11-field score snapshot for the current visitor. Two of those fields were practically designed for checkout work:
scores.conversionReadiness— 0–100, combining purchase intent with session signals. Intent alone says "this person wants the product"; readiness says "this session is the one where they buy."scores.decisionStage— a categorical read of where the visitor is in the decision:browsing,evaluating,comparing,deciding, orpurchasing. The five stages and how they're inferred get a full treatment in Beyond the Score: Emotional State and Decision Stage.
Both are part of the public snapshot every plan can read — behavioral inference from this session's events, not a profile lookup. (ClickStream's deeper models — regret analysis, purchase-timing prediction — stay dashboard-side; page code sees exactly the eleven documented fields.) Used together, the two fields map cleanly onto checkout treatments:
| decisionStage | What it suggests | Checkout treatment |
|---|---|---|
browsing | No active purchase process | Default checkout, unchanged |
evaluating | Working out whether this product fits | Reassurance rail: returns policy, guarantees, reviews stay visible |
comparing | Weighing you against alternatives | Reassurance rail, and don't trap them — comparison exits stay reachable |
deciding | Converging on a choice | Express candidate when conversionReadiness is also high |
purchasing | Actively transacting | Express: strip everything non-essential, defer offers to post-purchase |
The Express Path for Ready Buyers
A high-readiness visitor in deciding or purchasing has momentum. Every module between them and the confirmation screen — the cross-sell carousel, the "customers also bought" interstitial, the newsletter opt-in — is a chance to lose it. The express treatment gets out of the way:
- Skip the cross-sell interstitial entirely. Not "make it smaller" — skip it. The offer moves to the post-purchase page (more on that below).
- Lead with the fastest completion path. If your commerce stack knows this account has a saved payment method, surface that option first instead of burying it under the full card form. Signals doesn't know what's in the wallet — your backend does; Signals tells you this is the session to lead with it.
- Collapse the optional. Gift options, delivery notes, account-creation prompts fold behind disclosure toggles instead of occupying the critical path.
The gate is a one-shot read after configure() — browser keys are the cs_live_* family, which are domain-gated and safe to ship in page source:
import { configure, getVisitorOrNull } from '@clickstreamhq/signals';
configure({
apiKey: 'cs_live_xxx',
endpoint: 'https://t.example.com', // your verified first-party tracking domain
});
const visitor = await getVisitorOrNull();
// The default checkout is already in the HTML. Signals only decorates it.
if (
visitor &&
!visitor.bot.isBot &&
visitor.behavioralClass === 'human' &&
visitor.coverageMode !== 'degraded'
) {
const { conversionReadiness, decisionStage } = visitor.scores;
if (
conversionReadiness >= 70 &&
(decisionStage === 'deciding' || decisionStage === 'purchasing')
) {
document.documentElement.dataset.checkoutShape = 'express';
} else if (decisionStage === 'evaluating' || decisionStage === 'comparing') {
document.documentElement.dataset.checkoutShape = 'reassure';
}
}
Note the compound human gate: !visitor.bot.isBot covers network-level classification, behavioralClass === 'human' covers behavior, and the coverageMode check skips personalization when the server is serving a conservative placeholder. With the shape expressed as a data- attribute on the root element, the treatments are pure CSS:
/* Express: ready buyers skip merchandising, lead with the fast path */
[data-checkout-shape='express'] .checkout-cross-sell { display: none; }
[data-checkout-shape='express'] .saved-payment-entry { display: block; }
[data-checkout-shape='express'] .optional-fields { display: none; }
/* Reassure: deliberating visitors keep the trust rail */
[data-checkout-shape='reassure'] .trust-rail { display: block; }
No attribute, no change — the stylesheet's default state is the default checkout. That property does most of the fail-open work for free.
Defer Upsells to Post-Purchase
The instinct to upsell hardest at high readiness gets the logic backwards. A visitor at conversionReadiness 85 doesn't need persuading — they need a clear path. The interstitial you show them isn't capturing extra revenue; it's putting the revenue you already had at risk. For express sessions, move the offer to the confirmation page, where the purchase is banked and the visitor's attention is genuinely free. The deliberate shopper in comparing can still see in-flow merchandising if your tests support it — it's the ready buyer who shouldn't.
Reassurance for Deliberating Visitors
The mirror image matters just as much. A visitor in evaluating or comparing who has reached your checkout is doing diligence, not stalling — and this is where blanket "streamlining" backfires. Strip the returns policy, the guarantee, and the review summary from the checkout for everyone, and you've optimized for the ready buyer at the deliberator's expense.
The reassure shape keeps that content in a persistent rail beside the order summary: returns and refund policy, shipping expectations, the guarantee, a compact review summary. Nothing new is injected mid-session and nothing moves under the visitor's cursor — the rail is present in the default HTML and simply stays emphasized rather than being collapsed. Deliberation resolves on its own timeline; the checkout's job is to make sure the answers are on the page when it does.
The Guardrail: Same Price for Everyone
Now the rule that makes the rest of this defensible. Never gate price on a score. Not the price, not the total, not fees, not shipping tiers, not which discounts exist. A checkout that quietly charges — or offers — differently based on inferred readiness is a dark pattern: it converts a behavioral inference into price discrimination the visitor can't see or contest, and it torches trust the first time a customer compares receipts with a friend.
The line is easy to state and worth enforcing in review:
- Scores may change: layout, module ordering, emphasis, which reassurance content renders, when an offer appears (in-flow vs. post-purchase).
- Scores may never change: what anything costs, what the visitor can buy, which payment or shipping options exist, or which discounts they're eligible for.
Watch for price-gating in disguise, too. Hiding the promo-code field only for high-readiness sessions is a price decision wearing a layout costume — the ready buyer pays more on average because a score decided they wouldn't look for the field. If a module affects what the visitor pays, it renders for everyone or for no one.
Fail Open to the Default Checkout
A checkout is the worst possible place for personalization to become a dependency. The architecture above never lets it: the full checkout ships in the HTML, the purchase button works before any script runs, and Signals can only add a data- attribute after the fact. Three failure modes, three clean degradations:
- Signals unreachable or blocked:
getVisitorOrNull()returnsnullinstead of throwing. The condition never fires; the default shape renders. - Rate-limited: the client reuses the last good snapshot (marked
stale: true) for a bounded window, thengetVisitorOrNull()starts returningnull. Same outcome. - Signals Coverage exhausted: not an error at all. The server returns a
200placeholder withcoverageMode: 'degraded', all scores zeroed, andbehavioralClass: 'suspicious'— so aconversionReadiness >= 70gate fails closed even if you forget the explicitcoverageModecheck.
The full defensive playbook — stale reuse windows, placeholder semantics, what pending means — is in Fail-Open Personalization: Graceful Degradation Patterns. If you'd rather declare the rules than hand-roll the read, the effects engine expresses the same checkout in one call — the function form of when handles the two-stage OR that the criteria object (one decisionStage per rule) can't:
import { applySignals, effects } from '@clickstreamhq/signals/effects';
const sub = applySignals([
{
id: 'express-checkout',
when: (ctx) =>
!ctx.bot.isBot &&
ctx.behavioralClass === 'human' &&
ctx.coverageMode !== 'degraded' &&
ctx.scores.conversionReadiness >= 70 &&
(ctx.scores.decisionStage === 'deciding' ||
ctx.scores.decisionStage === 'purchasing'),
once: true,
run: effects.addClass('[data-checkout]', 'is-express'),
},
{
id: 'reassure-checkout',
when: { decisionStage: 'comparing', isBot: false },
once: true,
run: effects.addClass('[data-checkout]', 'is-reassure'),
},
]);
applySignals() defaults to the polling path, which is always fail-open; a failed effect routes through onError without killing the other rules. In React checkouts, the same gates live naturally in the useVisitor() hook's { ctx, loading, error } shape — render the default while loading, and on error just keep rendering it.
The Bottom Line
- Read two fields:
conversionReadiness(0–100) for whether this session is the one,decisionStagefor where the visitor is in the decision. - Ready buyers get out of the way: express shape, cross-sells skipped, fastest completion path first, offers deferred to post-purchase.
- Deliberators keep their answers: returns, guarantees, and reviews stay visible for
evaluatingandcomparingsessions. - Prices never move: a score may reorder the page, never change what anything costs — and promo-field hiding counts as a price change.
- The default checkout is complete:
getVisitorOrNull()plus a zeroed-score placeholder means every failure mode renders the checkout you'd have shipped anyway.
A checkout that adapts its shape earns conversions. A checkout that adapts its prices earns the screenshot thread that ends the brand. The eleven fields make the first one easy — the guardrail is what keeps you honest about the second.