Guide Me — integration
Status: plan, not implemented. Pick this up when ready. Captured: 2026-05-01.
Goal: ship the Guide Me survey as an embeddable widget that any FundedNext surface (landing pages, dashboard, model-specific pages, third-party partner sites) can drop in with one <script> tag — same pattern as Intercom, Google Tag Manager, Weglot.
Two embed modes:
- Modal overlay — single button click opens the survey as a fullscreen modal (like Intercom messenger).
- Inline container — host site allocates a
<div>; widget fills it (like a Weglot language switcher).
Architecture: iframe-based
The widget loads inside a sandboxed <iframe>. Total isolation: parent CSS cannot leak in, our CSS cannot leak out, and the React tree is independent so there's no version conflict with whatever the host runs. Updates ship from our side without partners needing to redeploy.
Public JS API
Auto-init via <script> (Intercom-style)
<!-- Drop this once in <head> on any page that needs the survey -->
<script async src="https://guide.fundednext.com/embed.js"
data-app-id="fundednext_main"
data-default-product="futures"></script>
<!-- Anywhere on the page: any element with data-guide-me="open" auto-binds -->
<button data-guide-me="open" data-product="cfd">Help me choose</button>The loader auto-scans for [data-guide-me] elements and wires them up. Re-scans on DOM mutation so dynamically-injected buttons work too.
Programmatic API
// Open as modal
GuideMe.open({ product: "futures", prefill: [/* optional */] });
// Mount inline into a host-allocated div
GuideMe.mount("#my-container", { product: "cfd", mode: "inline" });
// Close the open modal
GuideMe.close();
// Subscribe to events (mirror dataLayer events from the analytics plan)
GuideMe.on("started", (data) => {});
GuideMe.on("question_answered", (data) => {});
GuideMe.on("completed", (data) => { /* data: {primary_recommendation, confidence_pct, ...} */ });
GuideMe.on("email_submitted", (data) => {});
GuideMe.on("cta_clicked", (data) => { /* data: {cta_type, recommendation_id} */ });
GuideMe.on("closed", (data) => {});
// One-shot callback alternative
GuideMe.open({ product: "futures", onComplete: (rec) => {} });
// Identify the visitor (optional — for cross-session stitching when we have auth)
GuideMe.identify({ user_id: "u_123", email: "...", traits: { plan: "free" } });
// Update default config after init
GuideMe.config({ defaultProduct: "cfd", theme: "dark" });Configuration options
| Option | Source | Type | Default | Notes | ||
|---|---|---|---|---|---|---|
appId | data-app-id | string | required | Identifies the deployment (multi-tenant later) | ||
defaultProduct | data-default-product or config({}) | `'futures' \ | 'cfd' \ | null` | null (show market picker) | Skips Q0 |
prefill | open({prefill}) | PrefillItem[] | [] | Pre-answers given Q1/Q2 | ||
mode | mount({mode}) | `'modal' \ | 'inline'` | 'modal' for open, 'inline' for mount | ||
theme | data-theme or config({}) | `'dark' \ | 'light'` | 'dark' | Light theme is a future deliverable | |
locale | data-locale | 'en' (only for v1) | 'en' | Hooks for i18n later | ||
containerHeight | mount({containerHeight}) | number / 'auto' | 'auto' | Inline mode only | ||
entryPoint | data-entry-point or open({entryPoint}) | string | inferred from path | Tagged on every analytics event for funnel attribution |
postMessage protocol
All parent ↔ iframe traffic uses window.postMessage with strict origin checking.
Parent → iframe
type ParentMessage =
| { type: "guide-me:init"; payload: InitConfig }
| { type: "guide-me:close" }
| { type: "guide-me:identify"; payload: IdentifyTraits }
| { type: "guide-me:set-config"; payload: Partial<Config> };iframe → parent
type IframeMessage =
| { type: "guide-me:ready" } // iframe loaded, ready for init
| { type: "guide-me:resize"; height: number } // for inline mode auto-resize
| { type: "guide-me:event"; name: string; data: object } // funnel events
| { type: "guide-me:close-requested" } // user clicked × inside iframe
| { type: "guide-me:error"; message: string };Origin validation
- Parent script ignores any message whose
event.originisn't our published embed origin (e.g.https://guide.fundednext.com). - Iframe ignores any message whose
event.originisn't on its allowlist (initially*but CSP-restricted viaContent-Security-Policy: frame-ancestors).
Auto-resize for inline mode
Inline mode needs the iframe to grow as content grows (questions vary in height; recommendation card is taller than the question form).
- Inside iframe:
ResizeObserverwatchingdocument.body.scrollHeight. On change, postMessage{ type: "guide-me:resize", height }. - Parent loader sets
iframe.style.height = h + "px". - Throttle to 60 fps. Cap at viewport height; switch to internal scroll if exceeded.
Modal overlay behaviour
- Loader injects a hidden
<iframe>into<body>on firstopen()call (lazy — not on script load). - Iframe is
position: fixed,inset: 0,width: 100vw,height: 100vh,border: 0,z-index: 999999. - Backdrop is rendered inside the iframe (we already have
.fn-overlay) so the parent doesn't need to manage z-index conflicts. - ESC inside iframe sends
{ type: "guide-me:close-requested" }→ parent removes the iframe. - Body-scroll-lock on parent while modal is open.
Google Tag Manager integration
The loader pushes every survey event to window.dataLayer. Marketing teams configure GTM with standard "Custom Event" triggers — no extra code needed.
Events pushed to dataLayer
Every event lands in dataLayer with a consistent event name, prefixed guide_me_:
| dataLayer event | When it fires | Payload |
|---|---|---|
guide_me_loaded | Loader bootstrapped on the page | { app_id, entry_point } |
guide_me_opened | Modal opens or inline mounts | { product, entry_point, source } |
guide_me_started | First answer submitted | { product, session_id } |
guide_me_question_answered | After every answer | { session_id, question_idx, target, header, answer_value } |
guide_me_completed | Recommendation rendered | { session_id, primary_recommendation, confidence_pct, product, alt_count } |
guide_me_cta_clicked | Start Challenge / Discover button click | { session_id, recommendation_id, cta_type, dest_url } |
guide_me_email_submitted | Lead captured | { session_id, email_hash, opt_in } |
guide_me_closed | Modal dismissed | { session_id, completed: bool, last_step } |
guide_me_error | Stream / API error | { session_id?, message } |
Field rules:
session_idmatches the row in oursessionstable — paste it into support tickets to look up the live transcript.confidence_pctis the LLM's self-rated match, 0–100.email_hashis sha256(lowercased email) — never the raw email.entry_pointdefaults to the URL path; override withdata-entry-pointif you want pretty labels (e.g.pricing-page-cta,dashboard-banner).
Triggering Guide Me from GTM
Add a Custom HTML tag, fire it on any GTM trigger:
<script>
window.GuideMe && window.GuideMe.open({
product: {{Product From Page Var}}, // or hardcode 'futures' / 'cfd'
entryPoint: 'gtm_scroll_75'
});
</script>Common GTM triggers paired with this tag:
- Scroll depth ≥ 75% on the pricing page
- Exit intent (mouse leaves viewport, top edge)
- Click — All Elements matching
[data-cta="help-me-choose"] - Timer at 30 s on the landing page
- YouTube Video played past 50% (research-mode visitors)
Receiving Guide Me events in GTM
Set up one Custom Event trigger per dataLayer event you care about:
- Triggers → New → Custom Event.
- Event name:
guide_me_completed(or any from the table above). - Add Data Layer Variables for any field you want in tags:
DLV - primary_recommendation,DLV - confidence_pct,DLV - product,DLV - session_id,DLV - cta_type. - Wire those variables into your downstream tags (GA4, Meta CAPI, LinkedIn Insight, etc.).
GA4 mapping
| Goal | GA4 event name | GTM trigger | Suggested params |
|---|---|---|---|
| Funnel start | survey_start | guide_me_started | product, entry_point |
| Question depth | survey_progress | guide_me_question_answered | question_idx, target |
| Funnel complete | survey_complete | guide_me_completed | recommendation_id, confidence_pct, product |
| Lead generated | generate_lead | guide_me_email_submitted | opt_in |
| Checkout intent | cta_click | guide_me_cta_clicked | cta_type, dest_url, recommendation_id |
| Drop-off | survey_abandon | guide_me_closed (where completed = false) | last_step |
Mark survey_complete and cta_click as Conversions in GA4 → Admin → Events for funnel reporting.
Meta / LinkedIn / Reddit pixels
The same guide_me_completed Custom Event trigger can fire pixel "Lead" or "CompleteRegistration" events in parallel. Use Data Layer Variables (recommendation_id, product) as event parameters for ROAS attribution by recommendation class.
GTM custom template
We'll publish a .tpl template marketing imports once. Exposes friendly UI fields (product dropdown, optional prefill JSON, entry-point label) instead of asking marketers to hand-edit JavaScript.
Import path: GTM Templates → Tag Templates → Import. Once imported, marketers create new tags via the UI, with FundedNext-specific parameters pre-validated.
Cookie consent gating
Most GTM users gate analytics behind a cookie-consent CMP (OneTrust, Cookiebot). The loader honours window.OptanonActiveGroups / equivalent globals automatically — the iframe is only created once the visitor consents to the "functional" or "marketing" category. No extra GTM config needed; events stop firing in dataLayer until consent is granted.
Debugging in GTM Preview
- Open GTM Preview mode on the page that hosts the widget.
- Open the survey.
- The Tag Assistant tab lists every
guide_me_*event as it fires, with the full payload visible under Variables. - If an event isn't appearing: check the iframe origin matches the expected one and the loader has finished bootstrapping (
guide_me_loadedshould always be the first event).