Crossdeck Docs
Dashboard

Prove it yourself — the adversarial verification guide

Verification Audience: senior engineer evaluating Crossdeck for production revenue · ~15 min read · Updated May 31, 2026

We don't ask you to trust our numbers. We hand you the test. For every guarantee Crossdeck runs live, this page gives you the precise binary invariant, the exact surface where you verify it, the operation that triggers it, and — the part that earns a paying integration — the adversarial cases we'd run if we were trying to break it ourselves, plus the explicit boundary where each guarantee stops. A guarantee that names its own edge is worth more than one that claims perfection. The model is matched or held, never guessed: when Crossdeck can't be sure, it holds and tells you, it doesn't paper over.

TL;DR

The two surfaces

There are two places a guarantee is verified, and no single place shows everything. Knowing which is which is the whole game — most "I can't verify this" confusion is just looking at the wrong surface.

How to read an entry

Every entry has the same seven parts, so you can scan straight to the one you care about:

Verified in the log server-side · per project

These run server-side against your project's own records and write a timestamped pass/fail row to Settings → Contract Test every time. Do the action, refresh, and watch the result land. The verifier is read-only and re-derives its expectation independently of the code that wrote the data — so a green is a real second opinion, not the writer grading its own work.

Identity stitch

identity identity-stitch
Guarantee
When two records turn out to be the same person and you merge them, their subscriptions, revenue, and entitlements collapse into one. Nobody pays twice, nobody loses access, nothing is counted twice.
Invariant
After a merge: exactly one survivor holds the union; the loser is archived and points to the survivor; no subscription or purchase is orphaned; revenue is not double-counted; the survivor's entitlements cover every active subscription. All six hold, or it fails.
Where to verify
The contract log. Server-side, your project only.
How to test
Create the same person twice — an anonymous session, then sign in (or two browsers). Make a test purchase on one. Resolve the conflict in Settings → Conflicts, then Refresh the contract log.
Try to break it
  • Buy on both records before merging. Two live subscriptions for one human. After the merge, revenue must not double-count and the survivor's entitlements must still cover both subscriptions.
  • Chain the merges: merge A→B, then B→C. A subscription minted on A must still resolve to C through the chain — not read as orphaned. The verifier follows the merge chain at read time.
  • Race the repoint window. Fire an event on the loser record in the moment after you click merge but before the async repoint completes (its resolvedCdcust still points at the loser). This must read as mid-repoint / held, not as a red. A genuinely wrong attachment is red; a mid-flight one is held.
  • Comp the survivor a manual grant. Add an entitlement no subscription pays for. The contract must stay green — it asserts subscriptions are covered, not that entitlements are exactly the subscription set.
The boundary
It proves the mechanics of a merge are lossless and consistent — not that the human decision to merge was correct. "Are these two the same person?" is the operator's call, logged with a rationale. And it asserts the subset direction on entitlements (every active subscription is covered); it does not claim the survivor has only those entitlements, because comps, promos, and manual grants legitimately add more. We assert what's provably true and nothing wider.
What you'll see
A passing row with six green checks. A failure names the diverging check (e.g. revenue_no_double_count). A merge caught mid-repoint reads as held, not failed.

Error context

errors error-context
Guarantee
Every error attaches to the real person who hit it, positioned correctly in their journey — never a floating, unattributed exception.
Invariant
For each sampled error: the user resolves to a live customer (through any merge chain); the client timestamp is coherent with server receipt (client ≤ received + clock-skew allowance); a session is present.
Where to verify
The contract log. Server-side, your project only.
How to test
Trigger an error while signed in as a user. Open that user's timeline, then Refresh the contract log.
Try to break it
  • Throw before identifying. Error from an anonymous session, then sign in so the identity attaches afterward. The error must still resolve to the now-merged live customer through the chain, not read as orphaned.
  • Spoof the client clock. Send an error stamped far in the future. Position uses the client timestamp (when it happened to the user), but it's bounded against server receivedAt + a clock-skew allowance — a wildly future client time fails coherence rather than silently reordering the journey.
  • Error mid-merge. The user resolves through the chain to live | broken | pending; pending (mid-repoint) is held, not red. Only a genuinely unresolvable user is broken (red).
  • Strip the session. An error with no session fails session_present — a real red, as it should be.
The boundary
It proves the error is correctly attributed and positioned — not that your application's error message is right or its stack trace complete. Crossdeck verifies the plumbing (right person, right place, real session), not the semantics of what your code threw. Position is by the client clock; server receipt time is used only to bound clock skew and for dedup.
What you'll see
A passing row (user_resolves_to_live_customer, position_timestamp_coherent, session_present). A failure names the diverging check.

Journey fidelity

analytics journey-fidelity
Guarantee
The journey we show is the real path the user took — correctly ordered, no duplicates, with sessions that don't split when they navigate.
Invariant
Within a sampled customer's stream: no duplicate event ids; each session carries a single identity; that identity resolves live; gaps within a session stay inside the 30-minute window; session boundaries are ordered.
Where to verify
The contract log. Server-side, your project only.
How to test
Click through several pages across a navigation. View that visitor's journey, then Refresh the contract log.
Try to break it
  • Replay an event id. Fire the same event twice (a network retry or a double-fire). The duplicate must be collapsed — a repeated id fails no_duplicate_event_ids.
  • Switch identity mid-session. Sign in halfway through a session. The session must resolve to one identity through the merge chain (the survivor), not read as two — that's session_single_identity.
  • Idle past the window. Go quiet for 31 minutes, then act. That must open a new session, not stretch one across the gap.
  • Reorder arrival. Make events land at the server out of order. Ordering is by the client timestamp (when it happened), so boundary ordering still holds regardless of arrival order.
The boundary
It proves ordering, dedup, session integrity, and identity — it does not claim "no dropped events." Proving nothing was lost needs a client-side sequence number the event stream doesn't carry, so we don't assert it. We'd rather name the gap than imply a guarantee we can't verify.
What you'll see
A passing row across the five checks. A failure names which one diverged.

Proven in CI build time · every release

This one isn't toggle-and-watch, and we won't pretend it is. It's proven by a ground-truth fixture test in our CI on every release, because a live re-derive would be tautological — the same event stream computing the number and then checking itself. The fixture hand-specifies the inputs and asserts the exact output, so the arithmetic is pinned against a known-correct answer a human wrote.

Page CTR

analytics page-ctr
Guarantee
Click-through rate is computed honestly — unique clickers ÷ unique visitors, every click attributed to the page it actually happened on.
Invariant
CTR(page, action) = |unique customers who clicked that action on that page| ÷ |unique visitors to that page|. Unique over unique; clicks attributed to their own page.
Where to verify
CI, every release — a fixture test against the pure computePageJourney core.
How to test
Read the fixture and run the backend suite. The fixture specifies visitors and clicks and asserts the exact CTR — change the computation and the test goes red.
Try to break it
  • Double-click. The same visitor clicks the same action twice — counted once (unique clickers, not click count).
  • Click with no own-page. A click event that doesn't carry its own page is attributed to the visitor's current page, not dropped and not double-counted.
  • The window-shopper. A visitor who never clicks lands in the denominator (a visitor), never the numerator.
The boundary
It proves the arithmetic is honest — unique-over-unique, correct attribution — not that a high CTR means a good page. And it's CI-proven, not user-toggleable, because there's no independent live ground truth to re-derive against; we say that plainly rather than dress it up as a live check.
What you'll see
The CI run, green on every release. The fixture is the ground truth; the test fails the day the computation drifts.

Verified in your console client-side · live

These run inside your app on every Crossdeck call. Enable verification per app in the Apps tab, open your browser console, and act — they fire in real time. There's no stored per-project history here by design: passes are ephemeral, failures go to our central reliability channel so we harden the SDK for everyone. You verify these in the moment, watching them happen.

Per-user cache isolation

entitlements per-user-cache-isolation
Guarantee
Switching users never leaks the previous user's entitlements.
Invariant
identify(B) makes user A's cached entitlement slot physically unreachable; no isEntitled() after the switch reads A's cache.
Where to verify
Your browser console, live.
How to test
identify("A"), read entitlements; identify("B"); read again. Watch the cache key rotate in the console; A's grants are gone.
Try to break it
  • Ping-pong: identify(A) → identify(B) → identify(A) rapidly. The slot is keyed by identity; each switch evicts, so A-again is a fresh read, never B's residue.
  • Race the switch: call isEntitled() during the identify() transition. It reads the new identity's slot or misses — never the old user's grants.
  • Log out and back in: identify(null) then back to A. The anonymous slot can't serve A's prior cache.
The boundary
It isolates the client cache; the server is always the source of truth. It doesn't claim the network is instantaneous — it claims you never read a stale other user's grants from cache.
What you'll see
The console logs the cache key rotating on each identify(); the verifier fires green on every switch.

Idempotency-key determinism

revenue idempotency-key-deterministic
Guarantee
Retrying a purchase sync never double-charges or double-grants.
Invariant
The idempotency key is a pure function of the receipt/input — same input derives an identical key, every SDK, every retry.
Where to verify
Your browser console, live.
How to test
Call syncPurchases(receipt) twice with the same receipt. Watch the derived key match in the console.
Try to break it
  • Retry after a failure: kill the network mid-sync, then retry. The key is identical, so the backend collapses it to one effect.
  • Fire concurrently: two syncs of the same receipt at once. Same key → the server dedups.
  • Two devices, one receipt: the key is derived from the receipt, not device state, so it matches across installs.
The boundary
It guarantees the key is deterministic, so the server can dedup; the dedup itself is the server's job (and is contracted server-side). It doesn't stop the network from retrying — it makes retries safe.
What you'll see
The console logs the derived key; identical across calls with the same receipt.

Error-envelope shape

errors error-envelope-shape
Guarantee
Every API error comes back in one predictable shape, carrying a request id you can quote to support for an end-to-end trace.
Invariant
Every error response parses to a fixed envelope — code, message, requestId — with the request id always present. No untyped throws escape.
Where to verify
Your browser console, live.
How to test
Trigger an API error — a bad key works. Watch the SDK parse the envelope in the console.
Try to break it
  • Walk the codes: provoke a 401, a 429, a 500. Each yields the same envelope shape with a request id.
  • Malform the body / timeout: a garbled response or a network timeout surfaces as a typed network error, never a raw exception you have to guess at.
The boundary
It locks the shape and the presence of a traceable request id — not whose fault the error is. The request id is what lets support trace one failure end-to-end.
What you'll see
The console logs the parsed envelope with a requestId you can quote to support.

Flush-interval parity

diagnostics flush-interval-parity
Guarantee
Events flush on the same 2-second cadence across every SDK — no platform silently buffers longer.
Invariant
The live flush interval equals the code-pinned constant on boot, identical across SDKs, unless you explicitly override it.
Where to verify
Your browser console, live (the boot self-test).
How to test
Start the SDK. The boot self-test reports the live interval in the console.
Try to break it
  • Background the tab / throttle timers: the verifier reads the configured interval the loop actually runs on, not a documented number.
  • Override it: set a custom flush interval — the self-test reports your value, proving it reads reality, not a constant.
The boundary
It pins the default cadence parity across SDKs; you can override it deliberately. It doesn't promise delivery latency — it promises the flush loop runs on the interval it reports.
What you'll see
A boot console line with the live interval (the pinned default, or your override).

Super-property precedence

analytics super-property-merge-precedence
Guarantee
Your registered properties always win over auto-collected device fields — no silent clobbering of the values you set.
Invariant
On a key collision, the register()'d value takes precedence over the auto-collected one. Precedence order is fixed.
Where to verify
Your browser console, live.
How to test
register() a super-property, then track() an event with a colliding key. Watch your value win in the console.
Try to break it
  • Collide with an auto key (e.g. a device field): your explicit value still wins.
  • Re-register: register, then register the same key again — last explicit write wins, deterministically.
  • Register null: an explicit null is still an explicit value, applied as such, not silently dropped to the auto value.
The boundary
It guarantees precedence — your explicit values win — not the meaning of the value. It orders the merge; it doesn't validate your data.
What you'll see
The console shows the merged property set with your registered value present.

Payload schema-lock

diagnostics contract-failed-payload-schema-lock
Guarantee
Our own failure telemetry carries only diagnostic fields — never your users' personal data. The wire shape is locked.
Invariant
The crossdeck.contract_failed payload's field set ⊆ the locked allowedFields; no cdcust, no email, no PII; sdk_platform enum-locked to the known SDKs.
Where to verify
Your browser console, live (self-checks on every report).
How to test
Watch the payload's field set in the console on any contract_failed report. No personal data rides along.
Try to break it
  • Force a failure with user context in scope: the payload still contains only the allowed diagnostic fields — the lock forbids the rest by construction.
  • Try to attach a custom field: it doesn't reach the wire; the field set is fixed.
The boundary
It locks the wire shape of our reliability telemetry — the independent-controller channel that carries SDK failures from your app to us — keeping it PII-free. It's the structural backing for the lawful-basis analysis in Privacy Policy §6. Adding a field requires a pull request to this contract and a privacy amendment.
What you'll see
The console shows the payload — only the allowed diagnostic fields, never user data.

Error-codes catalogue

errors sdk-error-codes-catalogue
Guarantee
Every error code the backend can return has a plain-English meaning and a concrete fix, right in the SDK.
Invariant
Every backend error code resolves to a catalogue entry with remediation text. No orphan codes.
Where to verify
Your browser console, live (the boot self-test).
How to test
The boot self-test confirms every backend code resolves to remediation. Watch the count in the console.
Try to break it
  • Receive an unknown code: a code with no catalogue entry is the failure — the self-test flags the gap rather than passing silently.
  • Receive a deprecated code: still resolves to an entry; nothing falls off the back.
The boundary
It guarantees coverage and that remediation text exists — not that the remediation fixes your specific bug. Every code is documented; the fix is guidance, not a promise about your code path. See the canonical catalogue.
What you'll see
A boot console line confirming every code resolves (N of N).

What we deliberately don't claim

A guarantee is only as trustworthy as its stated edge. So, plainly, here is what these contracts do not assert — by design, because we can't prove it, or because it isn't ours to prove:

Matched or held, never guessed. When a check can't be made with certainty, the verifier holds and tells you — it doesn't fabricate a green. That's the whole point: a record you can trust because it admits its own edges.


Updated May 31, 2026. Every guarantee on this page runs live at HEAD of the monorepo's main branch. Contracts that aren't live yet aren't listed — we teach only what you can verify today.