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

# Build the invest flow

> Record a token-sale participation end-to-end with executeTokenSale - the SDK drives the ERC-20 approval and recording over the same EvmWallet you use for swaps.

Some Passage offers are **token sales**: the user commits a stablecoin (USDC, USDT, …) to buy into an offer, and the participation settles later on Passage's own schedule. The `coinlist.tokenSale` namespace runs that flow end-to-end - it reads the allowance, submits the ERC-20 `approve`, and records the participation - so you don't hand-roll the on-chain plumbing.

Like the [swap flow](/sdk/swap-flow), `coinlist.tokenSale` is **headless**: it drives the on-chain steps, but *you* bring the wallet through the same `EvmWallet` interface. One call, `coinlist.tokenSale.executeTokenSale`, is the token-sale twin of `executeSwap`.

<Note>
  Offers now carry a `type` field (`"sale"` or `"swap"`). Use `offer.type === "sale"` from [fetch offers](/sdk/fetch-offers) or [offer details](/sdk/sale-details) to route the user into this flow, and `"swap"` into the [swap flow](/sdk/swap-flow).
</Note>

## Prerequisites

* Completed [OAuth](/sdk/oauth-authentication) with a working `CoinListProvider`
* An `OfferDetail` and the `OfferOption` the user picked (from [Display offer details](/sdk/sale-details))
* The **funding contract address** for the offer. CoinList provides it during onboarding - keep it in a constant.
* A wallet integration exposed as an `EvmWallet`. The partner demo uses [wagmi](https://wagmi.sh/) + [Reown AppKit](https://reown.com/appkit); any stack that can send transactions works. The adapter is the same one the swap flow uses - see [Adapt your wallet to `EvmWallet`](/sdk/swap-flow#step-2-adapt-your-wallet-to-evmwallet).

```bash theme={null}
npm install @coinlist-co/react wagmi @wagmi/core viem @reown/appkit @reown/appkit-adapter-wagmi @tanstack/react-query
```

## Flow at a glance

```mermaid theme={null}
flowchart LR
  connect[connect_wallet]
  idle[enter_amount]
  investing[investing]
  success[success]
  error[error]

  connect -->|wallet connected| idle
  idle -->|user confirms amount| investing
  investing -->|approve mined + recorded| success
  investing -->|rejected / reverted| error
  error -->|retry| idle
```

1. **connect\_wallet** - your wallet UI; the SDK only needs the connected account.
2. **enter\_amount** - the user picks a funding asset (`offerDetail.fundingAssets`) and enters an amount.
3. **investing** - `coinlist.tokenSale.executeTokenSale` reads the allowance, runs the ERC-20 approval, and records the participation, reporting progress phases.
4. **success / error** - show the recorded participation, or a readable error.

<Note>
  Unlike the swap flow, a token sale needs **no wallet authorization step** - there's no ownership challenge to sign. The user only signs the ERC-20 `approve`. The funding move settles later on Passage's contracts, so nothing leaves the wallet during this flow.
</Note>

## Step 1: Pin the sale parameters

Keep the chain, funding contract, and payment token in one place. `TOKEN_REGISTRY` knows the ERC-20 metadata and per-chain address for USDC and USDT.

```ts constants.ts theme={null}
import {
  EvmContractAddress,
  type EthereumChain,
  TOKEN_REGISTRY,
  USDC_SYMBOL,
} from "@coinlist-co/react/shared";

export const SALE_CHAIN: EthereumChain = "ethereum_sepolia";

// The contract the approval grants an allowance to - the approve() spender.
// Provided by CoinList during onboarding.
export const FUNDING_CONTRACT_ADDRESS = EvmContractAddress(
  "YOUR_FUNDING_CONTRACT_ADDRESS",
);

// The stablecoin the user pays with (USDC, 6 decimals).
export const PAYMENT_TOKEN = TOKEN_REGISTRY.erc20(USDC_SYMBOL);
export const PAYMENT_TOKEN_ADDRESS = TOKEN_REGISTRY.contractAddress(
  USDC_SYMBOL,
  SALE_CHAIN,
);

// BCP-47 locale used for amount formatting.
export const LOCALE = "en-US";
```

<Note>
  This page runs on **Sepolia**. To move to mainnet, set `SALE_CHAIN` to `"ethereum_mainnet"` and point `FUNDING_CONTRACT_ADDRESS` at the mainnet contract CoinList gives you - `TOKEN_REGISTRY` already knows the mainnet USDC/USDT addresses.
</Note>

## Step 2: Adapt your wallet to `EvmWallet`

The token-sale flow drives **any** wallet through the same `EvmWallet` interface as the swap flow. It only ever calls `address`, `writeContract`, and `awaitTx` (there's no message to sign), so the adapter you already wrote for swaps works unchanged.

```ts theme={null}
import { buildEvmWallet } from "./wagmiEvmWallet"; // same adapter as the swap flow
```

If you haven't built it yet, copy [`wagmiEvmWallet.ts` from the swap flow](/sdk/swap-flow#step-2-adapt-your-wallet-to-evmwallet). Let your wallet library's errors propagate as-is - the SDK classifies them into a typed `WalletError` (`user_rejected`, `insufficient_funds`, `contract_reverted`, `timeout`, …).

## Step 3: Execute the sale

`executeTokenSale` runs the whole on-chain sequence and returns a discriminated result. The `amount` is a `BlockchainAmount` in the payment token's decimals - parse the user's input with `parseBlockchainAmount`.

```tsx theme={null}
const parsed = parseBlockchainAmount(amountInput, PAYMENT_TOKEN.decimals);
if (!parsed.valid || parsed.amount.raw <= 0n) return;

const result = await coinlist.tokenSale.executeTokenSale({
  wallet,
  offerId: offerDetail.id,
  offerOptionId: option.id,          // the OfferOption the user picked
  assetId: fundingAsset.id,          // funding asset from offerDetail.fundingAssets
  paymentTokenAddress: PAYMENT_TOKEN_ADDRESS,
  fundingContractAddress: FUNDING_CONTRACT_ADDRESS,
  chain: SALE_CHAIN,
  amount: parsed.amount,
  onProgress: (phase) => setBusy(phase),
});

if (result.type === "success") {
  // result: participation, approvalTxHash
  const explorerUrl = txExplorerUrl(SALE_CHAIN, result.approvalTxHash);
} else {
  // result.error.step: "allowance-check" | "allowance-reset"
  //   | "allowance-reset-reverted" | "approval" | "approval-reverted"
  //   | "participation"
  setError("The investment did not go through.");
}
```

Phases (wallet popups marked):

`checking-allowance` → *(`resetting-allowance` → `confirming-allowance-reset`, only for stale non-zero allowances)* → **`approving`** *(wallet popup)* → `confirming-approval` → `recording-participation`

<Note>
  An `approve` is submitted on **every** participation, even when the existing allowance already covers the amount: the backend requires a fresh approval transaction hash and verifies it on-chain before recording. USDT-style tokens with a stale non-zero allowance need one extra reset-to-zero transaction first, reported as the distinct `allowance-reset` phase.
</Note>

When recording fails after the approval mined (`error.step === "participation"`), the error carries the `approvalTxHash` so you can retry the record without asking the user to approve again.

## Putting it together

A complete, minimal invest component. It reuses `constants.ts` (Step 1) and `buildEvmWallet` (Step 2, shared with the swap flow).

```tsx MinimalInvest.tsx theme={null}
"use client";

import {
  type TokenSaleExecutionPhase,
  useCoinList,
} from "@coinlist-co/react";
import {
  type Asset,
  EvmWalletAddress,
  formatAmount,
  type OfferDetail,
  type OfferOption,
  parseBlockchainAmount,
  txExplorerUrl,
} from "@coinlist-co/react/shared";
import { useAppKit, useAppKitAccount } from "@reown/appkit/react";
import { useState } from "react";
import { useConfig, useWalletClient } from "wagmi";
import {
  LOCALE,
  FUNDING_CONTRACT_ADDRESS,
  PAYMENT_TOKEN,
  PAYMENT_TOKEN_ADDRESS,
  SALE_CHAIN,
} from "./constants";
import { buildEvmWallet } from "./wagmiEvmWallet";

const GENERIC_ERROR = "Something went wrong. Please try again.";

export function MinimalInvest({
  offer,
  option,
  fundingAsset,
}: {
  offer: OfferDetail;
  option: OfferOption;
  fundingAsset: Asset;
}) {
  const { coinlist, isReady } = useCoinList();
  const { open } = useAppKit();
  const { address, isConnected } = useAppKitAccount();
  const { data: walletClient } = useWalletClient();
  const config = useConfig();

  const [amountInput, setAmountInput] = useState("");
  const [busy, setBusy] = useState<TokenSaleExecutionPhase | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [confirmed, setConfirmed] = useState<{ explorerUrl: string } | null>(
    null,
  );

  const parsed = parseBlockchainAmount(amountInput, PAYMENT_TOKEN.decimals);
  const hasAmount = parsed.valid && parsed.amount.raw > 0n;

  const handleInvest = async () => {
    if (!isReady || !walletClient || !address || !hasAmount) return;
    setError(null);
    setBusy("checking-allowance");
    try {
      const wallet = buildEvmWallet({
        address: EvmWalletAddress(address),
        walletClient,
        config,
      });
      const result = await coinlist.tokenSale.executeTokenSale({
        wallet,
        offerId: offer.id,
        offerOptionId: option.id,
        assetId: fundingAsset.id,
        paymentTokenAddress: PAYMENT_TOKEN_ADDRESS,
        fundingContractAddress: FUNDING_CONTRACT_ADDRESS,
        chain: SALE_CHAIN,
        amount: parsed.amount,
        onProgress: setBusy,
      });
      if (result.type === "success") {
        setConfirmed({
          explorerUrl: txExplorerUrl(SALE_CHAIN, result.approvalTxHash),
        });
      } else {
        setError(GENERIC_ERROR);
      }
    } catch {
      setError(GENERIC_ERROR);
    } finally {
      setBusy(null);
    }
  };

  if (confirmed) {
    return (
      <div>
        <h3>Investment recorded</h3>
        <p>Your participation in {offer.name} is being processed.</p>
        <a href={confirmed.explorerUrl} target="_blank" rel="noreferrer">
          View approval transaction
        </a>
      </div>
    );
  }

  if (!isConnected || !address) {
    return <button onClick={() => open()}>Connect wallet</button>;
  }

  return (
    <div>
      <input
        inputMode="decimal"
        placeholder={`Amount in ${PAYMENT_TOKEN.symbol}`}
        value={amountInput}
        onChange={(e) => {
          if (e.target.value === "" || /^\d*\.?\d*$/.test(e.target.value)) {
            setAmountInput(e.target.value);
          }
        }}
      />

      {hasAmount && (
        <p>
          You invest {formatAmount(parsed.amount, LOCALE)} {PAYMENT_TOKEN.symbol}
        </p>
      )}

      <button
        onClick={handleInvest}
        disabled={!isReady || !walletClient || !hasAmount || busy !== null}
      >
        Invest
      </button>

      {busy && <p>{investPhaseLabel(busy)}</p>}
      {error && <p role="alert">{error}</p>}
    </div>
  );
}

function investPhaseLabel(phase: TokenSaleExecutionPhase): string {
  switch (phase) {
    case "resetting-allowance":
      return `Reset your ${PAYMENT_TOKEN.symbol} allowance in your wallet…`;
    case "approving":
      return `Approve ${PAYMENT_TOKEN.symbol} in your wallet…`;
    case "recording-participation":
      return "Recording your participation…";
    default:
      return "Working…";
  }
}
```

## Reflecting status back in the UI

After a successful `executeTokenSale`, use **[`useParticipations(offerId)`](/sdk/participations)** to fetch the user's participations for that offer and render their status (`pending`, `completed`, `failed`, `remitted`, etc.). The participation moves through statuses asynchronously - the SDK doesn't push; you re-fetch on session refresh or after a user action.

## Going further

* **Show the user's balance** - `coinlist.erc20.getTokenBalance({ tokenAddress, owner, chain })` reads the connected wallet's balance for a payment token, so you can validate the amount or render a Max button before investing.
* **Lower-level control** - if you drive the ERC-20 `approve` yourself (a custom wallet, batching, a different confirmation UX), skip `executeTokenSale` and record the participation directly with [`coinlist.tokenSale.createParticipation`](/sdk/participations), passing the `approvalTransactionHash` you obtained. `executeTokenSale` is the batteries-included version of exactly that sequence.

## Common questions

<AccordionGroup>
  <Accordion title="Why an approve instead of a transfer?">
    Passage settles the funding move on its own contracts, on its own schedule, after on-chain verification. The user only signs an allowance up to the amount they want to invest; nothing leaves their wallet until the Passage backend later pulls it. This pattern is cancellable (the user can revoke the allowance) and lets Passage batch transfers.
  </Accordion>

  <Accordion title="Why is an approval always submitted?">
    The backend hard-requires an approval transaction hash on every participation and verifies that exact transaction on-chain (sender, token, spender, and approved amount) before confirming. A participation recorded without a fresh approval always fails, so `executeTokenSale` submits one even when the standing allowance already covers the amount - the allowance read only decides whether a USDT-style reset-to-zero is needed first.
  </Accordion>

  <Accordion title="Do I need wagmi / Reown AppKit specifically?">
    No. Anything implementing the `EvmWallet` interface works - viem directly, Privy, ethers with a thin adapter. The token-sale flow only calls `address`, `writeContract`, and `awaitTx`; it never sees your wallet library. It's the same `EvmWallet` the swap flow uses.
  </Accordion>

  <Accordion title="What if recording fails after the approval mined?">
    The error comes back as `{ step: "participation", approvalTxHash }`. The approval is already on-chain, so retry `coinlist.tokenSale.createParticipation` with that hash rather than re-running the whole flow - the user doesn't need to approve again.
  </Accordion>

  <Accordion title="What about chains other than Ethereum?">
    The flow generalises: the same `EvmWallet` adapter maps each `EthereumChain` to a network, and `TOKEN_REGISTRY` knows the payment-token addresses per chain. Your account manager will confirm which chains and assets are enabled for your offers.
  </Accordion>
</AccordionGroup>

## Next step

<Card title="Display participation status" icon="wallet" href="/sdk/participations" horizontal>
  Use `useParticipations(offerId)` or `CoinListClient.tokenSale.fetchParticipations()` to render the user's participation status after they invest.
</Card>
