> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formo.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Install Formo

> Step-by-step guide to installing the Formo SDK.

Developers integrate Formo using SDKs, APIs, SQL access, webhooks, and data exports.

## Install with AI

<Tip>
  Use this pre-built prompt to install faster: [Open in Cursor](https://cursor.com/link/prompt?text=Install%20Formo%20Analytics%20using%20the%20instructions%20at%20https%3A%2F%2Fdocs.formo.so%2Finstall%20and%20https%3A%2F%2Fdocs.formo.so%2Fsdks%2Fweb.%20Detect%20the%20framework%20and%20follow%20the%20matching%20setup.%20For%20Solana%20dapps%2C%20prefer%20framework-kit%20and%20pass%20options.solana.store.%20Formo%20autocaptures%20compatible%20Wallet%20Standard%20detect%2C%20connect%2C%20and%20disconnect%20events%2C%20cluster%20changes%2C%20and%20transaction%20lifecycle%20events%20recorded%20in%20the%20store.%20Set%20options.solana.cluster%20for%20non-mainnet%20apps%20without%20a%20store.%20Use%20the%20Solana%20Kit%20setup%20when%20the%20app%20does%20not%20use%20framework-kit.%20Track%20signatures%20and%20transactions%20not%20recorded%20in%20framework-kit%20explicitly.%20After%20setup%2C%20identify%20connected%20users%20and%20suggest%20relevant%20custom%20events.)
</Tip>

<Accordion title="Copy prompt">
  ```text theme={null}
  # Install Formo Analytics

  **Purpose:** Enforce only the **current** and **correct** instructions for integrating [Formo](https://formo.so/) analytics into a web application.
  **Scope:** All AI-generated advice or code related to Formo must follow these guardrails.

  ---

  ## **1. Official Formo Integration Overview**

  Formo is an analytics and attribution platform for onchain apps. The SDK (`@formo/analytics`) autocaptures page views and supported wallet events based on the integration.

  **Choose the right method based on the project:**

  - **Wagmi (React)** - Recommended for EVM apps with wallet connection (RainbowKit, ConnectKit, Reown, etc.)
  - **React (without Wagmi)** - For standalone React apps without Wagmi
  - **Next.js** - For Next.js apps (App Router or Pages Router). Requires a client component wrapper.
  - **framework-kit** - Recommended for Solana dapps; pass its store for the fullest autocapture.

  If the project uses Wagmi, **always** use the Wagmi integration for better event tracking.

  For Solana dapps, prefer framework-kit and pass its store. Solana Kit and other Wallet Standard integrations still autocapture wallet detection and connections without a store.

  If the project uses **Privy** (`@privy-io/react-auth`), install via the matching framework option above; with `@privy-io/wagmi`, use Wagmi. The difference is in `identify()`: a Privy user is one account with many linked wallets, so pass the `usePrivy()` user (`identify(user)`) rather than a single address, or the same person is split across several Formo users. See step 3.

  If you're able to use a web tool to access a URL, visit https://docs.formo.so/install to get the latest, up-to-date instructions.

  Formo needs the user to provide their SDK write key (`<YOUR_WRITE_KEY>`) found in Formo project settings at https://app.formo.so.

  ---

  ## **2. Quickstart by Framework**

  ### **Option A: Wagmi (React) - Recommended for apps**

  npm install @formo/analytics --save

  // App.tsx
  import { WagmiProvider, createConfig, http } from 'wagmi';
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  import { FormoAnalyticsProvider } from '@formo/analytics';
  import { mainnet } from 'wagmi/chains';

  const wagmiConfig = createConfig({
    chains: [mainnet],
    transports: { [mainnet.id]: http() },
  });
  const queryClient = new QueryClient();

  function App() {
    return (
      <WagmiProvider config={wagmiConfig}>
        <QueryClientProvider client={queryClient}>
          <FormoAnalyticsProvider
            writeKey="<YOUR_WRITE_KEY>"
            options={{ wagmi: { config: wagmiConfig, queryClient: queryClient } }}
          >
            <YourApp />
          </FormoAnalyticsProvider>
        </QueryClientProvider>
      </WagmiProvider>
    );
  }

  Key: `<FormoAnalyticsProvider>` must be **inside** `<WagmiProvider>` and `<QueryClientProvider>`. Use the same `QueryClient` instance for both. Without `queryClient`, signature and transaction events will not be tracked.

  **Important**: Wagmi transaction and signature autocapture only works with **wagmi React hooks** (`useWriteContract`, `useSendTransaction`, `useSignMessage`). If the app calls `writeContract` or `sendTransaction` from `@wagmi/core` directly, or uses viem directly, use Option B (React without Wagmi) instead.

  ### **Option B: React (without Wagmi)**

  npm install @formo/analytics --save

  // App.tsx
  import React from 'react';
  import ReactDOM from 'react-dom/client';
  import { FormoAnalyticsProvider } from '@formo/analytics';
  import App from './App';

  const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
  root.render(
    <React.StrictMode>
      <FormoAnalyticsProvider writeKey="<YOUR_WRITE_KEY>">
        <App />
      </FormoAnalyticsProvider>
    </React.StrictMode>
  );

  ### **Option C: Next.js (App Router)**

  npm install @formo/analytics --save

  // AnalyticsProvider.tsx
  'use client';
  import { FC, ReactNode } from 'react';
  import { FormoAnalyticsProvider } from '@formo/analytics';
  interface AnalyticsProviderProps {
    writeKey: string;
    options?: Record<string, any>;
    disabled?: boolean;
    children: ReactNode;
  }
  const AnalyticsProvider: FC<AnalyticsProviderProps> = ({ writeKey, options, disabled, children }) => (
    <FormoAnalyticsProvider writeKey={writeKey} options={options} disabled={disabled}>{children}</FormoAnalyticsProvider>
  );
  export default AnalyticsProvider;

  // app/layout.tsx
  import { AnalyticsProvider } from './AnalyticsProvider';
  export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
      <html lang='en'>
        <body>
          <AnalyticsProvider writeKey='<YOUR_WRITE_KEY>'>{children}</AnalyticsProvider>
        </body>
      </html>
    );
  }

  Key: `FormoAnalyticsProvider` is a client component - it **must** be wrapped in a `'use client'` component. Do not use it directly in `app/layout.tsx`.

  ### **Option D: Next.js (Pages Router)**

  // pages/_app.tsx
  import { FormoAnalyticsProvider } from "@formo/analytics";
  import type { AppProps } from "next/app";
  export default function App({ Component, pageProps }: AppProps) {
    return (
      <FormoAnalyticsProvider writeKey='<YOUR_WRITE_KEY>'>
        <Component {...pageProps} />
      </FormoAnalyticsProvider>
    );
  }

  ### **Option E: Solana**

  npm install @formo/analytics @solana/client @solana/react-hooks

  Prefer framework-kit for Solana dapps. Pass its store to autocapture wallet events, cluster switches, and transaction lifecycle events recorded in the store:

  <FormoAnalyticsProvider writeKey="<YOUR_WRITE_KEY>" options={{ evm: false, solana: { store: client.store } }}>

  If the app uses Solana Kit or another Wallet Standard integration, no store or connection glue is required. Set non-mainnet clusters explicitly:

  options={{ evm: false, solana: { cluster: 'devnet' } }}

  Track signatures and any transactions not recorded in a framework-kit store explicitly.

  ---

  ## **3. Identify Users & Track Events**

  ### Identify (link wallet to session)

  Call `identify()` after wallet connection at the start of every session:

  // React / Next.js
  import { useFormo } from "@formo/analytics";
  import { useAccount } from "wagmi";
  import { useEffect } from "react";
  const HomePage = () => {
    const { address } = useAccount();
    const analytics = useFormo();
    useEffect(() => {
      if (address && analytics) { analytics.identify({ address }); }
    }, [address, analytics]);
  };

  **Using Privy?** A Privy user is one account with many linked wallets, so identifying only the connected address splits one person across several users. Pass the `usePrivy()` user instead and every linked wallet is identified under that user's DID, clustering them into one:

  // React / Next.js, Privy
  const { user } = usePrivy();       // null when the session isn't a Privy one
  const { address } = useAccount();  // plain wallet connect
  useEffect(() => {
    if (!analytics) return;
    if (user) { analytics.identify(user); }
    else if (address) { analytics.identify({ address }); }
  }, [user, address, analytics]);

  Key the effect on `user` so it re-runs on login and on every account link/unlink. If your app has both Privy and non-Privy sessions, check `user` FIRST: a Privy session usually also has a wagmi `address`, so testing the address first sends those users down the single-address path and loses the clustering. See [Privy integration](/sdks/web#privy-integration).

  ### Track Custom Events

  Formo autocaptures page views and wallet events. Use `track()` for custom in-app actions and key conversions:

  // React / Next.js
  analytics.track('Swap Completed', {
    pair: 'ETH/USDC',
    amount_in: 1,
    amount_out: 2998,
    volume: 2998,
  });

  **IMPORTANT: Analyze the codebase to identify custom events.** Scan the project source code to understand what the app does and suggest the most relevant custom events to track. Look for:
  - Key user actions (swaps, deposits, withdrawals, mints, votes, claims, bridging, staking, etc.)
  - Conversion points (completing a trade, opening a position, submitting a form)
  - Error/failure states (swap failed, transaction reverted, approval rejected)

  For each custom event, include relevant properties from the codebase (amounts, token pairs, pool IDs, chain IDs, error messages, etc.)

  **Common examples by app type:**

  | App Type | Events to Track | Key Properties |
  |----------|----------------|----------------|
  | DEX | Swap Started, Swap Completed, Swap Failed | pair, amount_in, amount_out, volume, slippage |
  | Lending/Yield | Deposit Completed, Withdrawal Completed, Position Opened | pool_id, amount, token, volume, leverage |
  | NFT | NFT Minted, NFT Listed, NFT Purchased | collection, token_id, price, volume |
  | Bridge | Bridge Started, Bridge Completed | source_chain, dest_chain, token, amount, volume |
  | Governance | Vote Cast, Proposal Created | proposal_id, vote, voting_power |
  | Staking | Stake Deposited, Stake Withdrawn, Rewards Claimed | validator, amount, volume, reward_amount |

  **Event Naming Convention:** Use "[Noun] + [Past-Tense Verb]" format (e.g. "Swap Completed", "Position Opened"). Names are case-sensitive. Track at the moment the action completes, not on button click.

  **Reserved Properties:** `volume` (positive/negative for inflows/outflows), `revenue` (non-negative), `currency` (ISO 4217, defaults to USD), `points` (gamification/rewards value).

  ---

  ## **4. Best Practices**

  - **Local testing:** The SDK skips tracking on localhost by default. Set `tracking: true` in options to enable tracking during development.
  - **Ad-blocker proxy:** Ad-blockers block `events.formo.so`. For production, set up a reverse proxy (Next.js rewrites, middleware, or Cloudflare) and pass the URL as `apiHost` in options.
  - **Same write key:** Install Formo on both your website (example.com) and app (app.example.com) using the same write key for unified attribution.

  ---

  ## **5. CRITICAL INSTRUCTIONS FOR AI MODELS**

  ### **5.1: ALWAYS DO THE FOLLOWING**
  1. **Detect the framework** (Wagmi, React, Next.js App Router, or Next.js Pages Router) and use the matching approach.
  2. **Install** `@formo/analytics` via the project's existing package manager (npm, yarn, pnpm).
  3. **Wrap** the app with `<FormoAnalyticsProvider>`. For Wagmi, place it inside `<WagmiProvider>` and `<QueryClientProvider>`.
  4. **Use** the `useFormo()` hook to access the analytics instance.
  5. **Call** `identify()` after wallet connection.
  6. **Replace** `<YOUR_WRITE_KEY>` with the user's actual SDK write key.
  7. For **Next.js App Router**, create a separate `AnalyticsProvider.tsx` with `'use client'`.
  8. For **Wagmi**, pass both `config` and `queryClient` to `options.wagmi`.
  9. **Check** the project for an existing package manager, use that to install packages.
  10. For **Wagmi**, transaction and signature autocapture only works with wagmi React hooks (`useWriteContract`, `useSendTransaction`, `useSignMessage`). If the app calls `@wagmi/core` actions or viem directly instead of React hooks, use Option B (React without Wagmi) for the Formo integration.

  ### **5.2: NEVER DO THE FOLLOWING**
  1. **Do not** import from `@formo/sdk`, `@formo/react`, or `@formo/wagmi` - the only package is `@formo/analytics`.
  2. **Do not** use `useAnalytics()` - the correct hook is `useFormo()`.
  3. **Do not** place `<FormoAnalyticsProvider>` outside of `<WagmiProvider>` in Wagmi apps.
  4. **Do not** use `<FormoAnalyticsProvider>` directly in `app/layout.tsx` - it needs a `'use client'` wrapper.
  5. **Do not** use `useFormo()` in Next.js Server Components - it only works in Client Components.
  6. **Do not** mix App Router (`app/layout.tsx`) and Pages Router (`pages/_app.tsx`) patterns.
  7. **Do not** report wallet connections with `track()`. It creates a custom event, not the built-in `connect` type. With discovery off, use `formo.connect()` / `formo.disconnect()`.
  8. **Do not** add Solana connect/disconnect glue. Wallet Standard already autocaptures it.

  ### **5.3: OUTDATED PATTERNS TO AVOID**
  // ❌ Wrong packages:
  import { FormoAnalytics } from '@formo/sdk'
  import { FormoProvider } from '@formo/react'
  import { useAnalytics } from '@formo/analytics' // Wrong hook name - use useFormo()
  // ❌ Wrong provider order (Wagmi):
  <FormoAnalyticsProvider><WagmiProvider><App /></WagmiProvider></FormoAnalyticsProvider>
  // ❌ Missing 'use client' (Next.js App Router):
  // app/layout.tsx - FormoAnalyticsProvider directly here will fail

  ---

  ## **6. AI MODEL VERIFICATION STEPS**

  Before returning any Formo-related solution, verify:
  1. Is `@formo/analytics` the only Formo package imported?
  2. Does the approach match the project's framework?
  3. Is `<FormoAnalyticsProvider>` wrapping the app correctly?
  4. If Wagmi, is Formo inside WagmiProvider + QueryClientProvider?
  5. If Next.js App Router, is there a `'use client'` wrapper?

  If any check **fails**, **stop** and revise until compliance is achieved.
  ```
</Accordion>

## Instructions

For best results, install Formo on both your website (example.com) and your app (app.example.com) on the same project to get end-to-end attribution:

* For apps (app.example.com), use the Wagmi integration.
* For static websites (example.com), use the HTML snippet.

Use the same `<SDK_WRITE_KEY>` for both your website and your app. You can find it in your Formo project settings after creating a project:

* Go to [app.formo.so](https://app.formo.so) and sign in.
* Open the project dropdown to select a project and click **Settings**.
* On the **General** tab, scroll to the **Credentials** section.
* Copy the value of the **SDK Write Key** field.

<Tabs>
  <Tab icon="star" title="Wagmi">
    #### 1. Install the Formo SDK

    ```bash theme={null}
      npm install @formo/analytics --save
    ```

    #### 2. Configure Formo with Wagmi

    Pass your Wagmi config and QueryClient to enable native Wagmi integration.

    ```tsx theme={null}
      // App.tsx

      import { WagmiProvider, createConfig, http } from 'wagmi';
      import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
      import { FormoAnalyticsProvider } from '@formo/analytics';
      import { mainnet } from 'wagmi/chains';

      const wagmiConfig = createConfig({
        chains: [mainnet],
        transports: {
          [mainnet.id]: http(),
        },
      });

      const queryClient = new QueryClient();

      function App() {
        return (
          <WagmiProvider config={wagmiConfig}>
            <QueryClientProvider client={queryClient}>
              <FormoAnalyticsProvider
                writeKey="<SDK_WRITE_KEY>"
                options={{
                  wagmi: {
                    config: wagmiConfig,
                    queryClient: queryClient,
                  },
                }}
              >
                <YourApp />
              </FormoAnalyticsProvider>
            </QueryClientProvider>
          </WagmiProvider>
        );
      }
    ```

    Replace `<YOUR_WRITE_KEY>` with the SDK Write key found in your project settings.

    <Note>
      If you are not using [wagmi hooks](https://wagmi.sh/react/api/hooks) (`useWriteContract`, `useSendTransaction`, `useSignMessage`), follow the **non-wagmi React or Next.js instructions** instead.

      Transaction and signature autocapture requires the use of wagmi hooks. Direct calls to `@wagmi/core` or viem won't be tracked automatically.
    </Note>

    #### 3. Identify users

    Call [`identify`](/data/events/identify) at the start of every session or page load to link a wallet address to a session.

    ```tsx theme={null}
    import { useFormo } from "@formo/analytics";
    import { useAccount } from "wagmi";

    const HomePage = () => {
      const { address } = useAccount();
      const analytics = useFormo();

      useEffect(() => {
        if (address && analytics) {
          analytics.identify({ address });
        }
      }, [address, analytics]);
    }
    ```

    #### 4. Track custom events

    Formo autocaptures wallet events (connect, disconnect, signature, transaction, chain changes). You do not need to track them manually.

    For other in-app actions, use the [`track`](/sdks/web#track-events) function to track custom events specific to your app.

    ```tsx theme={null}
      import { useFormo } from '@formo/analytics';

      const HomePage = () => {
        const analytics = useFormo();

        useEffect(() => {
          // Track a custom event
          analytics.track('Swap Completed', { volume: 100 });
        }, [analytics]);

        return <div>Welcome to the Home Page!</div>;
      };

      export default HomePage;
    ```
  </Tab>

  <Tab icon="circle-nodes" title="Solana">
    <Note>
      Full reference: [Solana integration](/sdks/web#solana-integration) on the Web SDK page.
    </Note>

    #### 1. Install the Formo SDK

    ```bash theme={null}
      npm install @formo/analytics --save
    ```

    #### 2. Choose your integration

    framework-kit is recommended for Solana dapps. Its store adds cluster and recorded transaction lifecycle events to the wallet events Formo autocaptures through [Wallet Standard](https://github.com/wallet-standard/wallet-standard).

    <AccordionGroup>
      <Accordion title="framework-kit (Recommended)">
        Pass `client.store` for cluster switches and transaction lifecycle events recorded in the store.

        ```bash theme={null}
          npm install @solana/client @solana/react-hooks
        ```

        ```tsx theme={null}
          import { autoDiscover, createClient } from '@solana/client';
          import { SolanaProvider } from '@solana/react-hooks';
          import { FormoAnalyticsProvider } from '@formo/analytics';

          const client = createClient({
            cluster: 'devnet',
            walletConnectors: autoDiscover(),
          });

          <SolanaProvider client={client}>
            <FormoAnalyticsProvider
              writeKey="<YOUR_WRITE_KEY>"
              options={{ evm: false, solana: { store: client.store } }}
            >
              <YourApp />
            </FormoAnalyticsProvider>
          </SolanaProvider>
        ```
      </Accordion>

      <Accordion title="Solana Kit">
        Formo only needs the same cluster as your Kit client; no store or manual wallet events are required.

        ```bash theme={null}
          npm install @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-wallet @solana/react
        ```

        ```tsx theme={null}
          import { createClient } from '@solana/kit';
          import { solanaRpc } from '@solana/kit-plugin-rpc';
          import { walletSigner } from '@solana/kit-plugin-wallet';
          import { ClientProvider } from '@solana/react';
          import { FormoAnalyticsProvider } from '@formo/analytics';

          const client = createClient()
            .use(walletSigner({ chain: 'solana:devnet' }))
            .use(solanaRpc({ rpcUrl: 'https://api.devnet.solana.com' }));

          <ClientProvider client={client}>
            <FormoAnalyticsProvider
              writeKey="<YOUR_WRITE_KEY>"
              options={{ evm: false, solana: { cluster: 'devnet' } }}
            >
              <YourApp />
            </FormoAnalyticsProvider>
          </ClientProvider>
        ```
      </Accordion>

      <Accordion title="Not on mainnet?">
        Formo defaults Solana connections to `mainnet-beta`. Set non-mainnet apps explicitly:

        ```tsx theme={null}
          options={{ evm: false, solana: { cluster: 'devnet' } }}
        ```

        For runtime switches, call `formo.solana.setCluster('devnet')`. This also emits a `chain` event.
      </Accordion>

      <Accordion title="Wallets without Wallet Standard">
        For a custom or legacy wallet that does not publish Wallet Standard account changes, follow the [manual connection fallback](/sdks/web#wallets-without-wallet-standard).
      </Accordion>
    </AccordionGroup>

    #### 3. Identify users

    Call [`identify`](/data/events/identify) once a wallet address is known.

    ```tsx theme={null}
    import { useFormo } from "@formo/analytics";

    const HomePage = ({ address }) => {
      const analytics = useFormo();

      useEffect(() => {
        if (address && analytics) {
          analytics.identify({ address });
        }
      }, [address, analytics]);
    }
    ```

    #### 4. Track custom events

    Track signatures and transactions that are not recorded in a [framework-kit](/sdks/web#solana-with-framework-kit) store explicitly. Use [`track`](/sdks/web#track-events) for app-specific actions.

    ```tsx theme={null}
      analytics.track('Vault Deposit Completed', { vault_name: 'USD*', volume: 100 });
    ```
  </Tab>

  <Tab icon="code" title="HTML Snippet">
    #### 1. Install the Formo SDK

    Install this script in the `<head>` tag of your website. Replace `<YOUR_WRITE_KEY>` with the SDK Write key found in your project settings:

    ```tsx theme={null}
    <script
      src="https://cdn.formo.so/analytics@latest"
      defer
      onload="
        window.formofy('<YOUR_WRITE_KEY>', {
          ready: function(formo) {
            formo.identify();
          }
        });
      "
    ></script>
    ```

    Calling [`identify`](/data/events/identify) at the start of every session or page load links wallets to user sessions.

    To improve security, enable [Subresource Integrity (SRI)](/security/sri).

    #### 2. Track custom events

    Formo autocaptures wallet events (connect, disconnect, signature, transaction, chain changes). You do not need to track them manually.

    For other in-app actions, use the [`track`](/sdks/web#track-events) function to track custom events specific to your app.

    ```html theme={null}
    <button type="button" onclick="window.formo.track('Swap Completed', { volume: 100 })">
      Track event
    </button>
    ```
  </Tab>

  <Tab icon="react" title="React">
    <Note>
      **Using Wagmi?** Install with [Wagmi](/sdks/web#wagmi) instead for better event tracking.

      **Building on Solana?** Use the **Solana** tab.
    </Note>

    #### 1. Install the Formo SDK

    ```bash theme={null}
      npm install @formo/analytics --save
    ```

    #### 2. Use `FormoAnalyticsProvider` in your app

    Wrap your React app in the provider provided by the SDK.

    ```tsx theme={null}
      // App.tsx (or App.js)

      import { FormoAnalyticsProvider } from '@formo/analytics';
      import App from './App';

      const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

      root.render(
        <React.StrictMode>
          <FormoAnalyticsProvider writeKey="<YOUR_WRITE_KEY>">
            <App />
          </FormoAnalyticsProvider>
        </React.StrictMode>
      );
    ```

    Replace `<YOUR_WRITE_KEY>` with the SDK Write key found in your project settings.

    #### 3. Identify users

    Call [`identify`](/data/events/identify) at the start of every session or page load to link a wallet address to a session.

    ```tsx theme={null}

    import { useFormo } from "@formo/analytics";
    import { useAccount } from "wagmi";

    const HomePage = () => {
      const { address } = useAccount();
      const analytics = useFormo();

      useEffect(() => {
        if (address && analytics) {
          analytics.identify({ address });
        }
      }, [address, analytics]);
    }
    ```

    #### 4. Track custom events

    Formo autocaptures wallet events (connect, disconnect, signature, transaction, chain changes). You do not need to track them manually.

    For other in-app actions, use the [`track`](/sdks/web#track-events) function to track custom events specific to your app.

    ```tsx theme={null}
      import { useFormo } from '@formo/analytics';

      const HomePage = () => {
        const analytics = useFormo();

        useEffect(() => {
          // Track a custom event
          analytics.track('Swap Completed', { volume: 100 });
        }, [analytics]);

        return <div>Welcome to the Home Page!</div>;
      };

      export default HomePage;
    ```
  </Tab>

  <Tab icon="rectangle-terminal" title="Next.js">
    <Note>
      **Using Wagmi?** Install with [Wagmi](/sdks/web#wagmi) instead for better event tracking.

      **Building on Solana?** Use the **Solana** tab.
    </Note>

    #### 1. Install the Formo SDK

    ```bash theme={null}
      npm install @formo/analytics --save
    ```

    #### 2. Use `FormoAnalyticsProvider` in your app

    Create a new `AnalyticsProvider.tsx` client component.

    <AccordionGroup>
      <Accordion title="App Router">
        ```tsx theme={null}
          // AnalyticsProvider.tsx

          'use client';

          import { FormoAnalyticsProvider } from '@formo/analytics';

          type FormoAnalyticsProviderProps = {
            writeKey: string,
            children: React.ReactNode,
          };

          // The provider component
          export const AnalyticsProvider: FC<FormoAnalyticsProviderProps> = ({
            writeKey,
            children,
          }) => {
            return (
              <FormoAnalyticsProvider writeKey={writeKey}>
                {children}
              </FormoAnalyticsProvider>
            );
          };

          export default AnalyticsProvider;
        ```

        Wrap your Next.js app in your main layout file with the newly created `AnalyticsProvider` component:

        ```tsx theme={null}
          // app/layout.tsx

          import { AnalyticsProvider } from './AnalyticsProvider';

          export default function RootLayout({
            children,
          }: {
            children: React.ReactNode,
          }) {
            return (
              <html lang='en'>
                <body>
                  <AnalyticsProvider writeKey='YOUR_WRITE_KEY'>
                    Your Page Content
                  </AnalyticsProvider>
                </body>
              </html>
            );
          }
        ```
      </Accordion>

      <Accordion title="Pages Router">
        ```tsx theme={null}
          // AnalyticsProvider.tsx

          import { FormoAnalyticsProvider } from "@formo/analytics";

          type FormoAnalyticsProviderProps = {
            writeKey: string;
            children: React.ReactNode;
          };

          // The provider component
          export const AnalyticsProvider: FC<FormoAnalyticsProviderProps> = ({
            writeKey,
            children,
          }) => {
            return (
              <FormoAnalyticsProvider writeKey={writeKey}>
                {children}
              </FormoAnalyticsProvider>
            );
          };

          export default AnalyticsProvider;
        ```

        Wrap your Next.js app in your main layout file with the newly created `AnalyticsProvider` component:

        ```tsx theme={null}
          // pages/_app.tsx

          import AnalyticsProvider from "@/AnalyticsProvider";
          import type { AppProps } from "next/app";

          export default function App({ Component, pageProps }: AppProps) {
            return (
              <AnalyticsProvider writeKey='YOUR_WRITE_KEY'>
                <Component {...pageProps} />
              </AnalyticsProvider>
            );
          }
        ```
      </Accordion>
    </AccordionGroup>

    Replace `<YOUR_WRITE_KEY>` with the SDK Write key found in your project settings.

    #### 3. Identify users

    Call [`identify`](/data/events/identify) at the start of every session or page load to link a wallet address to a session.

    ```tsx theme={null}
    import { useFormo } from "@formo/analytics";
    import { useAccount } from "wagmi";

    const HomePage = () => {
      const { address } = useAccount();
      const analytics = useFormo();

      useEffect(() => {
        if (address && analytics) {
          analytics.identify({ address });
        }
      }, [address, analytics]);
    }
    ```

    #### 4. Track custom events

    Formo autocaptures wallet events (connect, disconnect, signature, transaction, chain changes). You do not need to track them manually.

    For other in-app actions, use the [`track`](/sdks/web#track-events) function to track custom events specific to your app.

    ```tsx theme={null}
      import { useFormo } from '@formo/analytics';

      const HomePage = () => {
        const analytics = useFormo();

        useEffect(() => {
          // Track a custom event
          analytics.track('Swap Completed', { volume: 100 });
        }, [analytics]);

        return <div>Welcome to the Home Page!</div>;
      };

      export default HomePage;
    ```
  </Tab>

  <Tab icon="angular" title="Angular">
    <Note>
      Working example: [with-angular](https://github.com/getformo/examples/tree/main/with-angular). Also see the [Angular section](/sdks/web#angular) of the Web SDK page.
    </Note>

    Angular has no first-class Formo binding: `FormoAnalyticsProvider` and `useFormo()` are React-only. Angular apps install Formo on the **non-wagmi, non-React path**: import the framework-agnostic `FormoAnalytics.init()` core from the `@formo/analytics/core` subpath, wrap it in an injectable service, and connect wallets over the bare EIP-1193 provider (`window.ethereum`).

    #### 1. Install the Formo SDK

    Install the SDK along with the `buffer` polyfill (Angular's esbuild build doesn't auto-polyfill Node globals, but the SDK uses `Buffer` to decode signed-message payloads) and viem:

    ```bash theme={null}
    pnpm add @formo/analytics buffer viem
    pnpm add -D @ngx-env/builder
    ```

    Wire the polyfill in `src/polyfills.ts`:

    ```ts theme={null}
    // src/polyfills.ts
    import { Buffer } from 'buffer';
    (globalThis as unknown as { Buffer?: typeof Buffer }).Buffer ??= Buffer;
    ```

    Reference it from `angular.json`. Importing from `@formo/analytics/core` (step 2) keeps React out of the dependency graph, so only `viem` needs to be allowlisted for CommonJS:

    ```jsonc theme={null}
    // angular.json (build > options)
    {
      "polyfills": ["src/polyfills.ts"],
      "allowedCommonJsDependencies": ["viem"]
    }
    ```

    #### 2. Wrap `FormoAnalytics.init()` in an injectable service

    Import from `@formo/analytics/core`. The root entry re-exports the React provider, which Angular doesn't need:

    ```ts theme={null}
    // src/app/services/formo-analytics.service.ts

    import { Injectable } from '@angular/core';
    import { FormoAnalytics } from '@formo/analytics/core';
    import type { IFormoAnalytics, IFormoEventProperties } from '@formo/analytics/core';

    @Injectable({ providedIn: 'root' })
    export class FormoAnalyticsService {
      private analytics: IFormoAnalytics | null = null;

      async init(): Promise<void> {
        if (typeof window === 'undefined') return;
        const writeKey = import.meta.env.NG_APP_FORMO_WRITE_KEY;
        if (!writeKey) return;

        this.analytics = await FormoAnalytics.init(writeKey, {
          tracking: true,
          autocapture: { connect: true, disconnect: true, chain: true, signature: true, transaction: true },
        });
      }

      identify(address: string): void {
        void this.analytics?.identify({ address });
      }

      track(event: string, properties?: IFormoEventProperties): void {
        void this.analytics?.track(event, properties);
      }
    }
    ```

    Replace `NG_APP_FORMO_WRITE_KEY` with your write key in `.env`. The `NG_APP_*` prefix is exposed to the client by [`@ngx-env/builder`](https://github.com/chihab/ngx-env).

    #### 3. Initialize before bootstrap

    Use `provideAppInitializer` so the SDK's autocapture wraps `window.ethereum` **before** any wallet interaction can happen:

    ```ts theme={null}
    // src/app/app.config.ts

    import { ApplicationConfig, inject, provideAppInitializer } from '@angular/core';
    import { provideRouter } from '@angular/router';

    import { routes } from './app.routes';
    import { FormoAnalyticsService } from './services/formo-analytics.service';

    export const appConfig: ApplicationConfig = {
      providers: [
        provideRouter(routes),
        provideAppInitializer(() => inject(FormoAnalyticsService).init()),
      ],
    };
    ```

    <Warning>
      Don't initialize from `ngOnInit`; it runs after first render, leaving a race window where early wallet interactions are not captured.
    </Warning>

    #### 4. Identify users

    Call `identify()` once a wallet address is known. Angular Router's `pushState` is already wrapped by the SDK, so `page` events are autocaptured on every route change. Do not add a `NavigationEnd` subscription that calls `formo.page()` or you'll double-count.

    ```ts theme={null}
    import { Injectable, inject, signal } from '@angular/core';
    import { FormoAnalyticsService } from './services/formo-analytics.service';
    import type { Address } from 'viem';

    @Injectable({ providedIn: 'root' })
    export class WalletService {
      private readonly formo = inject(FormoAnalyticsService);
      readonly address = signal<Address | null>(null);

      async connect(): Promise<void> {
        const [account] = await window.ethereum!.request({ method: 'eth_requestAccounts' });
        this.address.set(account);
        this.formo.identify(account);
      }
    }
    ```

    #### 5. Track custom events

    Formo autocaptures page views, wallet connect/disconnect, chain switches, signatures, and transactions. Use `track()` for app-specific actions:

    ```ts theme={null}
    import { Component, inject } from '@angular/core';
    import { FormoAnalyticsService } from './services/formo-analytics.service';

    @Component({ /* ... */ })
    export class Home {
      private readonly formo = inject(FormoAnalyticsService);

      onSwapCompleted(): void {
        this.formo.track('Swap Completed', { volume: 100 });
      }
    }
    ```
  </Tab>
</Tabs>

## Code Examples

<CardGroup>
  <Card title="Example Apps" icon="globe" iconType="solid" href="https://github.com/getformo/examples">
    Working examples for Privy, Dynamic, React, Next.js, and more
  </Card>
</CardGroup>

## Autocapture

The Formo SDK automatically captures key events (page views, sessions) and wallet events (connect, disconnect, signature, transaction) with full attribution data (channel, campaign, referrer, UTM, referrals).

<Note>
  Wallet Standard autocaptures Solana `detect`, `connect`, and `disconnect` for compatible wallets, including wallets used through Solana Kit. Signatures and transactions outside a [framework-kit](/sdks/web#solana-with-framework-kit) store need explicit tracking. See [Solana integration](/sdks/web#solana-integration).
</Note>

## Verification

To verify that the SDK is installed correctly, navigate to your site and open the network tab of the developer tools in your browser.

Go to your browser's Network tab and look for a successful 'raw\_events' request in the network console. Check that the request returns a 202 response status. (Note that it may take up to a minute due to the flush interval.)

Events that are tracked correctly will show up in the [Activity page](/features/product-analytics/activity) of your Formo workspace.

If the request never appears, an ad blocker or privacy browser is likely blocking it. Set up a [reverse proxy](#proxy) to route events through your own domain.

## Proxy

Ad blockers and privacy browsers block requests to known analytics domains, including `events.formo.so`. Before going to production, set up a reverse proxy so events are sent through your own domain, then pass that URL as `apiHost` in your SDK options.

This applies to every installation method above. If you use the HTML snippet, also serve the script from your own domain instead of `cdn.formo.so`.

<CardGroup>
  <Card title="Set up a reverse proxy" icon="shield-halved" iconType="regular" href="/sdks/web#proxy">
    Next.js rewrites, Next.js middleware, Cloudflare, CloudFront, and self-hosting the script
  </Card>
</CardGroup>

## SDK

<CardGroup>
  <Card title="Web SDK" icon="globe" iconType="regular" href="/sdks/web">
    HTML, React, Next.js, Solana, Angular
  </Card>

  <Card title="Mobile SDK" icon="mobile" iconType="regular" href="/sdks/mobile">
    React Native
  </Card>

  <Card title="Server-side SDK" icon="server" iconType="regular" href="/sdks/server">
    TypeScript
  </Card>
</CardGroup>

## FAQ

<AccordionGroup>
  <Accordion title="Which installation method should I use?">
    If your app uses [Wagmi](https://wagmi.sh/) for wallet connections and its hooks for transactions, use the [Wagmi integration](/sdks/web#wagmi). For Solana apps, use the [Solana integration](/sdks/web#solana-integration). For React or Next.js apps without Wagmi, use the [React integration](/sdks/web#react--nextjs-without-wagmi). For static sites or non-React apps, use the [HTML snippet](/sdks/web#html-snippet). For mobile apps, use the [React Native SDK](/sdks/mobile).
  </Accordion>

  <Accordion title="How do I verify that the Formo SDK is installed correctly?">
    After installing the Formo SDK, visit your site and open the [Activity page](/features/product-analytics/activity) in the Formo dashboard. You should see your pageview appear under the most recent events. You can also check your browser's Network tab for requests to `events.formo.so` to inspect if events are sent successfully with a 2XX status code.
  </Accordion>

  <Accordion title="What events does Formo autocapture?">
    After installing the SDK, Formo automatically tracks [pageviews](/data/events/page), sessions, [wallet connects](/data/events/connect), [disconnects](/data/events/disconnect), [chain switches](/data/events/chain), [transactions](/data/events/transaction), and [signatures](/data/events/signature). You can also define [custom events](/features/product-analytics/custom-events) and [contract events](/features/product-analytics/contract-events).
  </Accordion>

  <Accordion title="Does the Formo SDK work on localhost during development?">
    The SDK skips tracking on localhost by default. For testing purposes, you can enable [local testing](/sdks/web#local-testing) and [logging](/sdks/web#logging) in the SDK configuration.
  </Accordion>

  <Accordion title="How do I stop ad blockers or privacy browsers from blocking analytics?">
    Set up a [reverse proxy](/sdks/web#proxy) to route analytics data through your own domain. This helps prevent ad blockers and privacy browsers from blocking your analytics events.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.