
Cross-Domain Tracking for Onchain Apps

Key takeaways
To keep UTMs and referrers from site to app, put your site, app, docs, and blog on subdomains of one root domain if you can. Formo shares its visitor cookie across subdomains by default.
Formo strips same-domain referrers, so a click from example.xyz to app.example.xyz reads as internal navigation, not as a referral.
Forward UTM and referral parameters on the links from your marketing site into the app. Browser session storage does not cross subdomains.
Wallet in-app browsers and separate root domains get their own visitor ID. The wallet address joins them after the wallet connects, but not before.
Embedded widgets in iframes on partner sites lose cookies. Use referral codes in the widget URL and onchain builder codes instead.
Cross-domain tracking keeps your UTMs and referrers attached to one visitor from your marketing site to your app. It also covers your docs, partner widgets, wallet browsers, and mobile apps. Subdomains of one root domain can share a first-party cookie, so they are the easy case: forward your campaign parameters on every link into the app. Separate domains, iframes, and wallet in-app browsers break the link. There, the connected wallet address is often the only key that can join the journey again.
What is cross-domain tracking for onchain apps?
Cross-domain tracking for onchain apps is the practice of keeping one visitor identity and one traffic source across every domain a user touches before and after a wallet connect. It covers subdomains (site, app, docs), separate domains, embedded widgets, in-app browsers, and mobile deep links. The goal: credit each swap to the campaign that started the journey.
Most crypto products split across hosts: a site builder for marketing, a framework for the app, a docs platform, and partner widgets. Each split is a place where a cookie, a referrer, or a UTM tag can disappear.
What does a typical onchain user journey look like?
A typical onchain user journey crosses three to five hosts before the first transaction. A user sees a post on X, lands on the marketing site, reads the docs, opens the app, connects a wallet, and signs a transaction in a browser extension wallet or a wallet's in-app browser. Each hop has a different risk of losing identity or source.
Hop | Example | What can break |
|---|---|---|
Social link to site | X or Telegram post to example.xyz | In-app browsers often send no referrer |
Site to app | example.xyz to app.example.xyz | Cookie scope; UTMs not forwarded |
Docs and blog | docs.example.xyz, blog.example.xyz | Hosted on a different platform without the SDK |
Partner widget | Swap or bridge widget on partner.com | Iframe storage is third-party |
Aggregator or wallet browser | Dapp opened inside a wallet or aggregator app | Separate cookie jar; new visitor ID |
Mobile deep link | Link opens a wallet app at your dapp URL | Query string dropped by the link format |
Wallet connect | User connects on app.example.xyz | None. This is where identity becomes durable |
Why does tracking break between domains?
Tracking breaks between domains because browsers isolate storage by site. A cookie set on one root domain cannot be read on another. Session storage is per origin, so it does not even cross subdomains. Referrer policies and in-app browsers trim or drop the referrer. Iframes on other sites get partitioned or blocked storage.
The main causes:
Cookie scope. A cookie set for
app.example.xyzonly is invisible onexample.xyz. A cookie set for.example.xyzworks on both. No cookie crosses toexamplefi.com. See first-party cookie and third-party cookie.Session storage. It is scoped to one origin and one tab (see MDN on sessionStorage). Data kept there on
example.xyzis not there onapp.example.xyz.Referrer loss. A strict
Referrer-Policy, an HTTPS to HTTP downgrade, or an in-app browser can remove the referrer.In-app browsers. X, Telegram, and wallet apps open links in their own browser with their own storage. The same person gets a new visitor ID there.
Iframes. A widget from your domain inside
partner.comruns in a third-party context. Many browsers block or partition its storage (see MDN on state partitioning).
How does Formo track visitors across subdomains?
Formo tracks visitors across subdomains with a first-party cookie set on the root domain by default. The visitor ID is shared between example.xyz, app.example.xyz, docs.example.xyz, and any other subdomain, as long as each one runs the SDK in the same Formo project. You do not need extra code for this.
What the docs specify:
Root-domain cookies by default. The
crossSubdomainCookiesoption istrueby default and sets identity cookies on the root domain, such as.example.com. Set it tofalseto scope cookies to the current host.Migration. The SDK moves existing host-scoped cookies to the root domain, so visitors are not double counted.
One project, one write key. Formo recommends the SDK on both the website and the app, in the same project with the same write key.
Internal referrers removed. Formo strips same-domain referrers, such as
blog.example.comtoapp.example.com, before channel classification, so internal navigation rolls up to Direct instead of Referrers.First-party only. Formo uses first-party cookies and sets no third-party or cross-site tracking cookies.
Per-host reports. The Origins breakdown groups traffic by protocol and domain, which helps when the app spans many subdomains.
One detail matters for campaign data. Inside one tab on one site, the SDK keeps the landing URL's UTM parameters, click IDs, and referral code, plus the first external referrer, and adds them to later events whose URL has none. A new UTM or click ID on a later URL replaces the stored value. That memory uses browser session storage, which does not cross to another subdomain. So forward the campaign parameters on your "Launch app" link.
Formo's docs describe subdomain sharing only. They do not describe a visitor ID handoff between two separate root domains, so treat example.xyz and examplefi.com as two visitors until the wallet connects.
How can the wallet address join journeys across domains?
The wallet address joins journeys across domains because it is the same on every host, browser, and device. Once a user connects on any of your properties, events that carry that address belong to one wallet profile. The address is a strong join key after connect. It cannot recover anything a visitor did before connecting on that browser.
How it works in Formo:
Connect and identify. The SDK captures the connect. Call
identifyafter a wallet connect or sign-in, and pass your user ID if you have accounts.Linking. Formo links wallets when they share the anonymous ID cookie (the same browser, across sessions and days) or the same
identifyuser ID. Linked Addresses on a profile lists wallets seen on the same browser; Clusters also join wallets through a shared user ID. Formo does not use IP addresses, fingerprinting, or onchain transfers to link wallets.Full-history attribution. A wallet profile's first touch and last touch use the wallet's full history. Each field is resolved on its own.
Onchain events. A contract event inherits the UTM parameters, referrer, and country of the same wallet's SDK session from the last hour. With no SDK session in that hour, the event is Direct.
The limits are real:
Pre-connect touches stay anonymous. Wallet attribution reads events that carry the address. A visit on another browser that never connected stays on its own anonymous profile.
One person, many wallets. A user may connect a hot wallet on desktop and a different wallet on mobile. Only a shared cookie or user ID links them.
Shared wallets. A Safe multisig or a team-run EOA (externally owned account) puts several people behind one address.
Privacy. A wallet address is public and links to a full onchain history. Treat it with the same care as other personal data.
See identity resolution and wallet clustering.
How do you keep attribution intact across every hop?
You keep attribution intact by sharing one visitor ID where the browser allows it, carrying campaign parameters where it does not, and identifying the wallet as early as the product allows. The steps below cover the site-to-app path first, then social links, widgets, and mobile.
Install the SDK on every host. Put it on the site, app, docs, and blog, in one Formo project with one write key.
Keep cross-subdomain cookies on. Leave
crossSubdomainCookiesat its default oftrue.Forward campaign parameters into the app. Copy
utm_*andreffrom the landing URL onto every link to app.example.xyz, so the app's first page view carries them.Set a referrer policy. Formo recommends
no-referrer-when-downgrade, set as a response header or a meta tag.Tag every social link. Put UTM parameters on X, Telegram, Discord, and Farcaster links, because in-app browsers often drop the referrer. For paid ads, Formo also uses click IDs for the channel when the referrer is missing.
Identify on connect. Call
identifywith the address, and your user ID if users log in.Give partners a referral code. Formo detects
ref,referral,refcode,af, andreferrerby default, and you can add your own parameter names or a path pattern.Add builder codes for embedded flows. If your widget appends an ERC-8021 builder code to transactions, Formo parses it from calldata with no setup.
What about widgets, in-app browsers, and mobile wallets?
Widgets, in-app browsers, and mobile wallets need separate fixes, because each runs in its own storage context. You cannot share a cookie into them. Instead, carry the source in the URL or in the transaction itself, and let the wallet connect join the identity later. The table shows what works today.
Scenario | What breaks | General approach | Formo support |
|---|---|---|---|
Swap or bridge widget in an iframe on partner.com | Cookies and storage partitioned or blocked | Put the partner ID in the widget URL; tag transactions | Referral parameters and builder code parsing |
Dapp opened in a wallet's in-app browser | New visitor ID; referrer often missing | Keep UTMs in the dapp URL inside the deep link | UTMs and referral codes read from the page URL |
Aggregator sends users to your app | Referrer may be the aggregator or empty | Ask the aggregator for a | Referral and UTM capture |
Link from X or Telegram | Referrer dropped by in-app browser | UTM-tag every link | UTM capture; click IDs for paid ads |
Deep link into your React Native app | No browser cookie at all | Put UTMs and | Mobile SDK reads UTMs and referral codes from deep links |
Web to app install | Install breaks the link | Use the store's referrer where one exists | Android Play Install Referrer supported; iOS not supported in the SDK |
Test each wallet's deep link before a campaign. A wrong format can drop the query string, and the UTMs with it.
Frequently asked questions
Does Formo track users across subdomains by default?
Yes. The crossSubdomainCookies option is on by default and sets the visitor cookie on the root domain, so example.xyz, app.example.xyz, and docs.example.xyz share one visitor ID. Install the SDK on each subdomain in the same project with the same write key.
Why does my app show Direct traffic after users click from my website?
Formo strips same-domain referrers, so a click from example.xyz to app.example.xyz counts as internal navigation and reads as Direct. The original source sits on the landing page view. Forward UTM parameters on your app links to keep the source on the app's first page view as well.
Can Formo link visitors across two different root domains?
Not by cookie. Formo's documented sharing covers subdomains of one root domain. Across separate root domains, each domain has its own visitor ID. When the same wallet connects on both, events that carry that address land on one wallet profile.
Do UTM parameters survive a wallet's in-app browser?
Only if the URL that opens in the wallet still contains them. The in-app browser has its own storage, so earlier visits in another browser do not carry over. Put UTMs in the dapp URL inside the deep link and test the link before launch.
Is the wallet address a good cross-domain join key?
It is the best key after connect, because it is the same everywhere. It does not cover visits before the connect, it can map one person to many wallets, and it can map one wallet to a team. Pair it with an identify user ID when users have accounts.
How do I attribute swaps from a widget on a partner site?
Give each partner a referral code in the widget URL, and append an ERC-8021 builder code to the widget's transactions. Formo detects referral parameters in the page URL and parses builder codes from transaction calldata.


