
Installation
There are several ways to install the Formo SDK:- Wagmi is recommended for EVM apps
- Solana for Solana apps
- HTML Snippet is recommended for static websites
- React & Next.js (without Wagmi)
- Angular for Angular apps using the bare EIP-1193 provider
Wagmi
If you’re already using Wagmi, this is the recommended way to install Formo for apps.When
wagmi options are provided, the SDK hooks directly into Wagmi’s state management instead of wrapping EIP-1193 providers. This provides:- Native event handling via Wagmi’s built-in state system
- Better compatibility with wallet connection libraries (RainbowKit, ConnectKit, etc.)
- Full tracking of signatures and transactions via TanStack Query’s mutation cache
walletClient.sendTransaction, .signMessage, .writeContract), those calls create no wagmi mutation and are not captured by default; opt in with wagmi: { eip1193Fallback: true } (SDK 1.38.0+) and the SDK instruments the active connector’s provider with the same request wrapper the default mode uses. Hook-driven calls are never double-counted. The flag is off by default so wagmi mode never touches the signing transport without an explicit, auditable decision.HTML Snippet
Install this snippet at the<head> of your website:
React & Next.js (without Wagmi)
Use Wagmi for more reliable and secure event tracking.
Angular
FormoAnalyticsProvider and useFormo() are React-only. Angular apps import from the React-free @formo/analytics/core subpath and wrap FormoAnalytics.init() in an injectable service, with wallets connected over the bare EIP-1193 provider (window.ethereum). Full working example: with-angular.buffer polyfill. Angular’s esbuild build doesn’t auto-polyfill Node globals, but the SDK uses Buffer to decode signed-message payloads. Without it, signing throws ReferenceError: Buffer is not defined.
FormoAnalytics.init() in an injectable service. Import from @formo/analytics/core; the root entry pulls in the React provider, which Angular doesn’t need:
provideAppInitializer so the SDK’s autocapture wraps window.ethereum before any wallet interaction:
Identify users
Callidentify() after a user connects their wallet or signs in on your website or app:
- Wagmi
- Privy
- HTML Snippet
- Angular
Call
identify() in a useEffect that triggers when the wallet address changes:Track events
The Web SDK automatically captures common events such as page views and wallet events (connect, disconnect, signature, transaction, etc.) with full attribution (referrer, UTM, referrals). You do not need to manually track them.To track custom events (in-app user actions, key conversions) use the
track function with details of what happened:
idempotency_key property.
Code examples
Privy
examples/with-privy
Dynamic
examples/with-dynamic
Turnkey
examples/with-turnkey
React
examples/with-react
Next.js (app router)
examples/with-next-app-router
Next.js (pages router)
examples/with-next-page-router
MetaMask SDK
examples/with-metamask
Thirdweb
examples/with-thirdweb
Reown
examples/with-reown
Crossmint
examples/with-crossmint
Openfort
examples/with-openfort
Tempo
examples/with-tempo
Angular
examples/with-angular
Solana
examples/with-solana
React Native
examples/with-react-native
Configuration
Local testing
The SDK skips tracking on localhost by default. To enable tracking locally during development, settracking to true:
Logging
Control the level of logs the SDK prints to the console with the following logLevel settings:logger.enabled: false); no logs print unless you explicitly set logger.enabled: true. The levels array is matched exactly, not hierarchically: setting levels: ["warn"] shows only warn messages, not warn and error together. List every level you want to see.
Autocapture
You can configure which wallet events are automatically captured by the SDK:Batching
To support high-performance environments, the SDK sends events in batches.flushAt and flushInterval configuration parameters.
Ready callback
Theready callback function executes once the Formo SDK is fully loaded and ready to use. This is useful for performing initialization tasks or calling SDK methods that require the SDK to be ready.
onload attribute:
Environments
You can control tracking behavior in different environments (test, staging) with thetracking option:
Excluding by timezone
When the visitor’s browser-resolved timezone (viaIntl.DateTimeFormat().resolvedOptions().timeZone) matches an entry in excludeTimezones, the SDK suppresses tracking entirely for that visitor.
No events are enqueued or sent, including identify and connect, and no identity cookie is written. This is useful for opting whole regions out of analytics (for example, to honor a jurisdiction’s privacy expectations).
Excluding query parameters
Formo automatically captures the page URL, query string, individual query-parameter properties, and the referrer. If your URLs carry sensitive data (auth tokens, one-time codes, emails), usetracking.excludeQueryParams to strip those parameters in the browser, before any event is sent so they are never transmitted or stored:
privy_oauth_code: Privy OAuth authorization codeprivy_oauth_state: Privy OAuth CSRF state tokenprivy_oauth_provider: Privy OAuth provider identifier
url, query, referrer, and other event properties. Matching is case-insensitive.
Consent management
The Formo Web SDK includes simplified consent management functionality to help you comply with privacy regulations like GDPR, CCPA, and ePrivacy Directive. Control user tracking preferences with simple opt-out and opt-in methods:Cross subdomain tracking
By default, Formo sets identity cookies on the root domain, sharing visitor identity across all subdomains. This ensures accurate visitor counts and consistent attribution across your subdomains out of the box.
With the default
crossSubdomainCookies setting:
- Cross subdomain tracking: track users as they move between your marketing site, app, docs, and other subdomains.
- Accurate attribution: attribute conversions to the correct channel, even when users cross subdomains.
- Automatic migration: the SDK migrates existing host-scoped cookies to the apex domain so visitors are not double-counted.
crossSubdomainCookies to false:
- HTML Snippet
- React / Next.js
- JavaScript
Referrer URL tracking
Referrer URL tracking lets you understand the specific URL where your users are coming from, as long as the site has the correctReferer-Policy header set.
We recommend setting the no-referrer-when-downgrade policy, which will only send the domain, path, and query parameters to the destination URL as long as the protocol security level stays the same (i.e. HTTP → HTTP, or HTTPS → HTTPS).
If the protocol security level is downgraded (i.e. HTTPS → HTTP), the referrer will not be sent. This is a good compromise to make sure your users’ privacy is respected while still getting a good amount of data.
Here’s how to configure this:
- Next.js
- HTML
If you are using Next.js, you can use the headers property in your
next.config.js file to configure the referrer policy:Referrals
Formo autodetectsref, referral, refcode, af, and referrer query parameters in the URL. You can customize how referrals are detected by the SDK:
- the
viaandrefquery parameters in the URL, and - from the
/referral/([^/]+)path pattern
Wagmi integration
For apps using Wagmi, enable native integration by providing the Wagmi config and QueryClient:- Hooks directly into Wagmi’s state management for connection events
- Uses TanStack Query’s mutation cache for signature and transaction tracking
- Skips EIP-1193 provider wrapping (no proxy behavior)
Solana integration
- framework-kit (Recommended): pass its store for wallet, cluster, and recorded transaction lifecycle events.
- Solana Kit: wallet
detect,connect, anddisconnectare autocaptured through Wallet Standard without a store.
@solana/kit and framework-kit are different projects. Solana Kit provides the core client and plugins; framework-kit provides the separate @solana/client and @solana/react-hooks packages and is built on top of Kit.
Solana with framework-kit
For Solana dapps, use framework-kit (@solana/client) and pass client.store. Wallet events hand off to the store without double-counting, while the store adds cluster switches and transactions recorded in it. React transaction hooks do not write to the store and still need explicit tracking.
See the focused framework-kit example.
- React
- Without React
Pass Then use the
client.store in the Solana options. This adds network changes and transactions recorded in the store; hook-based transactions still need explicit tracking.useFormo hook in any component:
Wallet
detect events still come from Wallet Standard discovery.
Solana Kit
If your app uses Solana Kit directly, give Formo the same cluster. Formo observes Wallet Standard independently, so it does not need the Kit client or a store.Other Wallet Standard integrations
The SDK observes Wallet Standard account changes without wrapping wallet methods or adding a Solana dependency.@solana/wallet-adapter-react example needs only FormoAnalyticsProvider. Discovery works whenever the connected wallet registers with the page through Wallet Standard:
Cluster
Wallet Standard does not expose your app’s active cluster, only the clusters a wallet supports. Formo reports
mainnet-beta when the wallet supports it, and otherwise the first Solana cluster the wallet lists. Set non-mainnet apps explicitly:
formo.solana.setCluster('devnet'). This also emits a chain event. See manual transaction and signature tracking.
Wallets without Wallet Standard
Most Solana wallets require no manual connection tracking. If a custom or legacy wallet does not publish Wallet Standard account changes, callformo.connect() and formo.disconnect() when its state changes.
Emit connect from wallet state, including restored sessions, and disconnect when the account is revoked:
Solana chain IDs
Solana has no numeric chain ID, so Formo maps each cluster to a reserved ID above 900000 to avoid colliding with EVM chains. Use the exported constant rather than hardcoding the number.
Mainnet has several spellings: Solana Kit’s wallet plugin takes
solana:mainnet, framework-kit’s client takes mainnet, and Formo uses mainnet-beta. Map between them when deriving one from another.
connect() validates the address against the chain ID, so a base58 Solana address paired with an EVM chain ID is rejected. Rejected calls log a warning and emit nothing. Enable logging while integrating so you see them:
Manually tracking Solana transactions and signatures
Track Solana Kit signatures and transactions explicitly. Framework-kit transactions are autocaptured only when they are recorded in the supplied store; its React hooks (useSolTransfer, useSendTransaction, useTransactionPool) do not write to that store.
For Solana-only apps, set
evm: false to disable EVM provider detection (EIP-1193 / EIP-6963). This prevents unnecessary tracking of injected EVM wallets.WalletConnect integration
Using Wagmi (RainbowKit, ConnectKit, AppKit, Privy, and most React dapps)? WalletConnect is already tracked through the Wagmi integration; skip this section.
registerProvider refuses in Wagmi mode to prevent double-reporting.window.ethereum, which covers injected browser wallets only. A WalletConnect provider is different: your app constructs it in code with EthereumProvider.init(...), so the SDK cannot find it on its own. Hand it over with registerProvider (SDK 1.38.0+):
- Call it as soon as the provider exists, before or after the wallet connects; an already-connected session is adopted on the spot.
- Events name the wallet behind WalletConnect (“Ledger Live”, “MetaMask Wallet”) from the session’s peer metadata.
- Returns
truewhen capture works;falsewhen it refuses (Wagmi mode, EVM tracking disabled, or not a valid EIP-1193 provider). - The same call works for any other provider your app constructs rather than discovers; pass
{ name, rdns }as a second argument to label a custom one.
Privy integration
A Privy user is one account (a DID) with many linked wallets - an embedded wallet plus any external wallets they connect over time. Because Formo is address-keyed, one person with 8 wallets would otherwise appear as 8 users.identify(user) resolves this in a single call: it expands user.linkedAccounts and emits one identify per linked wallet, each tagged with the same Privy DID, so they cluster into one user.
user.linkedAccounts - privyDid, privyCreatedAt, email, phone, and socials (X, Discord, GitHub, Farcaster, Google, …) - plus that wallet’s own wallet_client, chain_type, and is_embedded.
Linked wallets include wallet and smart_wallet accounts, plus a cross_app account’s embedded and smart wallets (e.g. Abstract Global Wallet). Addresses are deduplicated.
Apps with both Privy and non-Privy users. Branch on which identity you have, checking user first - a Privy session usually also has a wagmi address, so testing the address first would send that user down the plain path and lose the clustering:
parsePrivyProperties(user) returns the same parsed profile and wallet list without emitting, if you want to inspect or display what would be sent.
Proxy
To handle ad-blockers and privacy browsers, we recommend setting up a reverse proxy.
What happens under the hood when you forward the data to Formo via a reverse proxy
CDN
Some ad blockers block specific scripts and API calls based on the domain name and URL. To address this issue you can download and serve the Formo HTML Snippet fromcdn.formo.so to your own domain, e.g. yourdomain.com/formo.js.
This only applies to the HTML snippet. If you installed @formo/analytics via NPM, the SDK is bundled with your app and is not loaded from the CDN, so you can skip this step.
Next.js rewrites
If you are using Next.js, you can take advantage of rewrites to behave like a reverse proxy. To do so, add arewrites() function to your next.config.js file:
apiHost parameter in the Formo SDK:
See an example Next.js app here.If you are using the HTML / website snippet, replace the install script with:
Next.js middleware
If you are using Next.js and rewrites aren’t working for you, you can write custom middleware to proxy requests to Formo. Create a file namedmiddleware.js/ts in your base directory (same level as the app folder).
Others
Alternatively, you can set up your own proxy (with CloudFront, Cloudflare, etc.) and pass the URL as the SDKapiHost:
Verification
To verify the proxy is working:- Visit your website / app
- Open the network tab in your browser’s developer tools
- Check that analytics requests are going through your domain instead of
events.formo.so - Check that events show up in the Activity page on the Formo dashboard
FAQ
What is the difference between the Wagmi integration and the standard React integration?
What is the difference between the Wagmi integration and the standard React integration?
The Wagmi integration automatically tracks wallet connects, disconnects, chain switches, transactions, and signatures by hooking into Wagmi’s wallet adapter. The standard React integration tracks wallet events by wrapping the EIP-1193 wallet provider to track signatures and transactions. Use the Wagmi integration if your app uses Wagmi and its hooks.
How do I track custom events like swaps, deposits, or mints?
How do I track custom events like swaps, deposits, or mints?
Use formo.track() to send custom events with any properties you need. For example:
formo.track('Swap Completed', { pair: 'ETH/USDC', token_in: 'ETH', token_out: 'USDC', amount_in: 1.5 }). Custom events appear in the Activity feed and can be queried in the Explorer.Does the Formo SDK support consent management for GDPR?
Does the Formo SDK support consent management for GDPR?
Yes. The SDK provides built-in consent management with
optOutTracking() and optInTracking() methods. Call optOutTracking() to stop all tracking for a user, and optInTracking() to re-enable it. Formo does not use third-party cookies, IP addresses, or device fingerprinting, so most jurisdictions do not require a cookie consent banner.Can I use Formo with Privy or other embedded wallet providers?
Can I use Formo with Privy or other embedded wallet providers?
Yes. The SDK includes a dedicated Privy integration: pass the
usePrivy() user to identify(user) and every wallet linked to that Privy account is identified under the user’s DID, so a multi-wallet user clusters into one Formo user. For other wallet providers, use the standard React integration. If your wallet provider uses Wagmi under the hood (many do, including Privy), you can also use the Wagmi integration for automatic wallet event tracking.Do I need framework-kit to use Formo on Solana?
Do I need framework-kit to use Formo on Solana?
framework-kit is recommended for Solana dapps because its store supplies cluster switches and recorded transaction lifecycle events. It is not required: Wallet Standard still autocaptures
detect, connect, and disconnect for compatible wallets used through Solana Kit or other integrations. Signatures and transactions outside the store require explicit tracking.