Top One Futures × Axcera · Integration Spec · Revision 3

TraderArea DataLayer

What we would like pushed to window.dataLayer on the Top One Futures TraderArea, so that offers, prompts and in-app messaging can be built and iterated entirely on our side without another platform release. Top One Trader follows on the identical schema once this is proven.

Revised 8 Sep 2026 TraderArea portal.toponefutures.com Container GTM-K7XMHV9M (new; portal loads none today) Phase 1 one event, no transitions
Scope, please read first

This launches on Top One Futures first. The container shared so far, GTM-KXQQ6G4S, belongs to the Top One Trader TraderArea at app.toponetrader.com. The Futures TraderArea at portal.toponefutures.com currently loads no GTM container at all, so step one is installing one there. The container is GTM-K7XMHV9M, created in Top One's own GTM account; please embed it on the Futures tenant the same way GTM-KXQQ6G4S is embedded in the Trader app. Everything below is written so the identical schema ships to Trader afterwards without changes.

§1 Principle

Push facts, not offers

We agree with making this global rather than building it around a single breach event, and we have gone further in that direction: phase 1 is one event.

Axcera pushes facts about the user and their accounts. Status, timestamps, plan, size, counts. These change rarely and are stable to implement against.

Top One decides what is shown. Which traders see a prompt, what it says, what it offers, for how long, and when it stops. That logic changes weekly during a campaign, and every one of those changes should cost you nothing.

If a field would ever need updating because we changed a promotion, it is on the wrong side of the line. Nothing in this document carries a price, a discount, a coupon, a deadline or an eligibility rule.

Why the names match your read API

The fields below reuse the names and meanings from your prop-accounts read API because it makes your implementation cheap: serialise the agreed fields from objects you already have. It is not a substitute for freshness. Before we show an offer, and again at redemption, we revalidate the account against your API server-side, so a stale page can never grant anything.

§2 Shape

One snapshot event, re-pushed whenever the facts change

A single event, topone_context, carrying a complete snapshot under its own topone namespace. It is a picture of the current state, never a "something just happened" signal. We derive everything we need from it, including whether an account is breached.

// Initialise once, before the first push. Never reassign an existing dataLayer.
window.dataLayer = window.dataLayer || [];

// Clear only our namespace, then push a COMPLETE snapshot. GTM keeps the last value of any
// key that a later push omits, so a partial push would let a stale account list survive.
window.dataLayer.push({ topone: null });
window.dataLayer.push({
  event: "topone_context",
  topone: {
    schemaVersion:  1,
    brand:          "topone_futures",
    environment:    "production",
    pageInstanceId: "9c1f4e2a",          // new per full page load
    sequence:       12,                  // increases with every push in this page instance
    observedAt:     "2026-09-08T18:00:00Z",
    page:  { navigationId: "n_0007", type: "account_detail", state: "ready" },  // navigationId: new per client-side navigation
    user: {
      userId:            "5f3a9c1e2b7d4a6081c2e9f0",
      accountsTotal:     7,
      accountsActive:    2,
      accountsBreached:  4,
      accountsFunded:    1,
      breachesLifetime:  9,
      firstPurchaseAt:   "2026-03-11T09:02:14Z",
      lastPurchaseAt:    "2026-09-02T16:27:41Z"
    },
    account: {                            // the account in context, else null
      accountId:    "64d1f0a2b3c4e5f6a7b8c9d0",
      accountNr:    "TOF-PX-100001",
      status:       "breached",
      phase:        "evaluation",
      planId:       "elite-daily-50k",
      plan:         "Elite Daily | $50,000",
      accountSize:  50000,
      platform:     "tradovate",
      createdAt:    "2026-08-29T21:40:03Z",
      breachedAt:   "2026-09-03T10:30:00.412Z",  // native precision, never truncated
      isReset:      false,               // native meaning: a reset was performed on this account
      resetTo:      null,                // replacement account id once reset
      resetAt:      null,
      isRepeated:   false,  repeatedTo: null,
      isUpgraded:   false,  upgradedTo: null,
      updatedAt:    "2026-09-08T17:58:41.007Z"  // when your record last changed
    },
    accounts:      null,                   // array of the same objects on list pages
    accountsScope: "not_loaded"            // "all" | "visible_page" | "not_loaded"
  }
});

When to push it

Every push carries all of user, account and accounts, using null when not applicable. accountsScope says whether an array is the whole portfolio, one visible page, or not loaded at all; an empty array [] means a known empty result. The counts on user are backend totals and never depend on what is visible.

§3 Fields

Envelope

FieldTypeDefinition
schemaVersionintegerStarts at 1. A renamed key, changed type, changed nullability or changed meaning is a new version. Added keys are not.required
brandenumtopone_futures now, topone_trader later. Keeps one tag safe across both sites.required
environmentenumproduction or staging, so a test environment can never trigger a live offer.required
pageInstanceIdstringNew on every full page load. With sequence, lets us discard out-of-order or duplicate snapshots.required
sequenceintegerIncrements on every push within a page instance.required
observedAtISO 8601When this snapshot was assembled in the browser. Not a freshness guarantee; our server revalidates before anything is shown.required
page.navigationIdstringNew for every client-side navigation, so account A to account B is two instances even though the route type is the same. Both the loading and the ready snapshot of one navigation carry the same id.required
page.typeenumPlease send the full list. We need at least dashboard, account list, account detail, checkout, payment, other.required
page.stateenumloading or ready. We act only on ready.required

User object

FieldTypeDefinition
userIdstringYour stable customer id. Our key for joining, capping and dedupe. Always a string, even if numeric.required
accountsTotalintegerAll accounts this user has ever owned, any status.required
accountsActiveintegerAccounts currently tradable. Please state exactly which statuses count; pending activation must not, since that person needs an activation prompt, not a win-back.required
accountsBreachedintegerAccounts currently breached and not yet reset. A point-in-time count.required
breachesLifetimeintegerBreach occurrences across the user's history, null if unknown. Not needed for launch and not worth reconstructing history for; we can derive it from your read API ourselves.nice
accountsFundedintegerAccounts currently in a funded phase. Not "ever funded".required
firstPurchaseAt / lastPurchaseAtISO 8601Paid account purchases only: not resets, activation fees, giveaways or replacements. null for a user who has never bought, which is different from a lapsed customer.nice

Account object

FieldTypeDefinition
accountIdstringYour account id. It is already stamped on the orders we receive, so it ties a prompt to a purchase end to end.required
accountNrstringThe number the trader recognises, e.g. TOF-PX-100001. Shown so they know which account we mean.required
statusenumThe state we react to. We need the complete list of values you can emit. Any value we do not recognise suppresses the campaign for that account and raises a diagnostic on our side; we never guess.required
phaseenumevaluation or funded. Resets are different products at different prices in each.required
planIdstringStable, enumerable identifier we map to a product. Full list needed.required
accountSizeintegerAccount size in whole US dollars. 50000, never "$50,000".required
breachedAtISO 8601When this account breached, at your native precision. null when not breached.required
resetTostringThe replacement account created by a reset, null until then. Your read API already exposes this, which suggests a reset creates a new account rather than reusing the id. If that holds, the breached accountId is the breach occurrence and we need no separate breach id. Please confirm with the examples in section 9.required
isReset / resetAtbool / ISOExactly your native meaning, not ours: a reset was performed on this account, and when. We will not redefine an existing API field; if the native meaning differs from what we assume here, the examples in section 9 will surface it.required
isRepeated / repeatedTo
isUpgraded / upgradedTo
bool / stringNative lineage fields, same meaning as your read API. They let us walk an account's chain (reset, repeat, upgrade) without inventing counters.nice
updatedAtISO 8601When your record last changed. It tells us the record's age, not whether the copy we received is current, so our server revalidation stays regardless.required
planstringHuman-readable plan name for the message body. Rendered as text only, never as markup.nice
platformenumTrading platform. Full list needed.nice
createdAtISO 8601Account creation time.nice

Please serialise only the fields agreed here rather than your whole API object, so a future field added to your API does not silently become part of this contract.

§4 Later

Transition events, only where a snapshot cannot express them

Phase 1 needs none of these. They are listed so the envelope you build now accommodates them without a second design round. Each is a moment in time and must carry its own occurrence id, because an account object alone cannot distinguish two payouts or two breaches.

account_pending_activation
An account passed and awaits activation. The activation prompt you already proposed as the second scenario. Carries the account object.
account_passed
A phase was cleared. Carries the account object and the phase completed.
account_reset_completed
Carries the breached accountId and resetTo, so we can close exactly the prompt that was open.
payout_completed
Needs payoutId, completedAt, the account and a definition of "completed". Best moment for a review or referral ask.

Anything that must be captured while the trader is offline, such as a reset or payout for durable history, belongs in a server-to-server webhook rather than the browser. That is a separate conversation and does not affect phase 1.

§5 Pages

Every authenticated route, including the ones we will not act on

The snapshot fires on every authenticated TraderArea route. We would rather receive it on checkout and payment pages with page.type saying so than receive nothing there, because nothing cannot take a prompt down. Our tag suppresses on those types and removes anything already mounted.

The routes where a prompt will actually appear are the dashboard, the account list and the account detail page, with that account in account so a prompt can be specific to the one the trader is looking at.

§6 Conventions

Details that save a round trip

Shared understanding

The dataLayer is client-side. A trader can push any event from a browser console, and any script on the page can read what is there. So it only ever decides what is displayed. Which offer a trader is entitled to, its deadline and its terms are issued by a Top One endpoint that checks your API and our order records, and the checkout revalidates the same offer before granting anything. Nobody on either side should treat the dataLayer as a security boundary.

§7 Our side

What Top One commits to, so you can hold us to it

§8 First test

Smoke test, then acceptance

  1. You embed GTM-K7XMHV9M on the Futures TraderArea and implement topone_context with the phase 1 fields.
  2. You send us one named test account, its id, and confirm it is on our server allowlist, so the offer endpoint answers only for that user during testing.
  3. We verify in GTM Preview, which stays scoped to our session, and confirm every field back to you.
  4. We build the tag, keep it in Preview until it is right, then publish behind a kill switch.

The single-account test proves the plumbing. Before we widen it, we will exercise on our side: first breach, reset, second breach on the replacement account; account A to account B with a slow response for A still in flight; several breached accounts and a paginated list; detail to dashboard to checkout; logout and login as another user; slow or out-of-order responses while the dashboard stays open; two tabs and two devices; storage unavailable; expiry during checkout and a duplicate redemption attempt; and the published CSP outside Preview. Agreed that testing happens on production and that anything published is live immediately.

§9 Back to you

What we need before the first test

  1. Install the Futures container. portal.toponefutures.com loads no GTM today. Please embed GTM-K7XMHV9M on the Futures tenant (head snippet plus noscript frame, as on Trader) and tell us which routes, if any, are excluded. The container lives in Top One's account and publish rights stay with our team.
  2. Enum values for status, phase, platform, planId and page.type.
  3. Reset semantics. Confirm the native meaning of isReset, resetTo and resetAt with a concrete example of each: a first breach, a completed reset, a second breach, and a reset that creates a replacement account. We will adopt your meaning rather than impose one.
  4. Refresh channels. Which changes reach an open dashboard by polling or WebSocket, and can the snapshot be re-pushed when they land?
  5. Content Security Policy. The policy actually enforced on the TraderArea: whether GTM runs with a nonce, and what script-src, style-src, connect-src, img-src and font-src allow. Our loader, its stylesheet and our offer endpoint need to be permitted, and Preview mode has its own requirements.
  6. Identity we can trust. Required, not optional. A browser-supplied userId is not authentication, and your read API can confirm ownership but not who is asking. We need one of: a same-origin endpoint on the TraderArea that returns a short-lived signed assertion for the logged-in session (a JWT over userId and tenant, audience topone-offers, about five minutes, keys held server-side on both ends), which our script passes directly to our endpoint and never into the dataLayer; an authenticated proxy on your side; or an existing session mapping we can verify. The assertion proves identity only; eligibility is still checked live.