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

# Custom UI Integration

> Fund any market with any asset from any chain, using your own components

Use this guide when you want your own UI — asset selector, amount input, buttons, status — and the SDK
only as the execution engine. This is the pattern our live production integrations use. The
prebuilt modals (`SpiceSupply`, `SpiceDeposit`) package this same pipeline; use them instead if you
want less code.

The pattern, per market:

1. Describe the market's token as a `destinationToken`.
2. Write one `buildActionCalls` function returning the market's on chain calls (approve + supply).
3. `useSupplyAssets` lists every asset the user can fund it with, across all supported chains.
4. `useSupplyQuote` resolves routing (swap needed or direct) for the selected source asset.
5. `estimateFeePreview` produces the fee quote — surface it before enabling the submit button.
6. `executeSupply` runs the whole flow: funding, routing, delegation signing, solver execution.

The user never bridges and never needs gas on the source chain — the Spicenet solver executes via
EIP 7702 and covers gas on the routing legs, accounted for in the fee quote. If your app already
sponsors gas on the destination chain (e.g. Privy gas sponsorship on Base), keep it: it applies to
your app's own transactions and needs no SDK configuration.

## Provider Setup

`SpiceFlowProvider` does not mount Privy, wagmi, or react-query — it reads wallet state from those
contexts. Your app mounts all four, in this order:

```tsx theme={null}
import "@spicenet-io/spiceflow-ui/styles.css"; // required once, at app entry

import type { ReactNode } from "react";
import { WagmiProvider, createConfig, http } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { PrivyProvider } from "@privy-io/react-auth";
import { SpiceFlowProvider } from "@spicenet-io/spiceflow-ui";
import { getMainnetChains } from "@spicenet-io/spiceflow-core";
import { defineChain, type Chain } from "viem";

// Every chain the installed SDK supports; new chains arrive with SDK upgrades.
const chains = getMainnetChains().map((c): Chain =>
  defineChain({
    id: c.id,
    name: c.displayName,
    nativeCurrency: c.nativeCurrency,
    rpcUrls: { default: { http: [c.rpcUrl] } },
    blockExplorers: { default: { name: "Explorer", url: c.blockExplorer } },
  }),
) as [Chain, ...Chain[]];

const wagmiConfig = createConfig({
  chains,
  transports: Object.fromEntries(chains.map((c) => [c.id, http()])),
});

const queryClient = new QueryClient();

export function Providers({ children }: { children: ReactNode }) {
  return (
    <WagmiProvider config={wagmiConfig}>
      <QueryClientProvider client={queryClient}>
        <PrivyProvider
          appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}
          config={{
            supportedChains: [...chains],
            embeddedWallets: { ethereum: { createOnLogin: "all-users" } },
          }}
        >
          {/* supportedChainIds omitted: SpiceFlowProvider defaults to every supported chain */}
          <SpiceFlowProvider provider="privy" network="mainnet" nativeChainId={8453}>
            {children}
          </SpiceFlowProvider>
        </PrivyProvider>
      </QueryClientProvider>
    </WagmiProvider>
  );
}
```

<Warning>
  One `chains` array feeds wagmi, Privy, and the SDK, so the three stay consistent by construction.
  If you restrict to specific chains instead, keep all three in sync — a chain missing from the
  `PrivyProvider` config fails at wallet-client creation with "Chain X is not configured in
  PrivyProvider". If you already run Privy, reuse your existing `PrivyProvider` — just add the source
  chains you enable. For production, put your own RPC URLs in the wagmi `transports` — the bare
  `http()` falls back to each chain's public endpoint, which rate-limits under real traffic.
</Warning>

<Note>
  The `styles.css` import is required even with a fully custom UI — the Privy login modal renders
  unstyled (invisible) without it. On Vite, also polyfill Node's `Buffer` global before the SDK loads;
  cross-chain swap quoting references it at runtime.
</Note>

## Complete Example

A supply panel for one market. Verified against `@spicenet-io/spiceflow-ui@4.7.9`.

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

import { useRef, useState } from "react";
import { type Address } from "viem";
import {
  useWallet,
  useSpiceExecution,
  useSupplyAssets,
  useSupplyQuote,
  buildSupplyChainBatches,
  buildPayChainBatches,
  estimateFeePreview,
  executeSupply,
  type Asset,
  type BatchBuilderContext,
  type BuildActionCallsContext,
  type Call,
  type ExecutorContext,
  type SupplyStep,
  type SupplyQuoteResult,
} from "@spicenet-io/spiceflow-ui";

const destinationToken = {
  chainId: 8453,
  address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as Address, // USDC on Base
  symbol: "USDC",
  decimals: 6,
};

// One function per market: the destination chain calls to run once funds arrive.
// ctx.userAddress is the wallet the action runs as.
const buildActionCalls = (amount: bigint, ctx: BuildActionCallsContext): Call[] => [
  { to: destinationToken.address, value: 0n, data: encodeApprove(MARKET, amount) },
  { to: MARKET, value: 0n, data: encodeSupply(destinationToken.address, amount, ctx.userAddress) },
];

export function SupplyPanel() {
  const { address, isReady, isAuthenticated, isConnected, provider } = useWallet();
  const { execute, estimate } = useSpiceExecution();

  // Every asset the user can fund this market with, across all chains
  const { displayAssets, embeddedWalletAddress, externalAddress, isNon7702 } =
    useSupplyAssets({ address, destinationToken, enabled: true });

  const [selected, setSelected] = useState<Asset | null>(null);
  const [amount, setAmount] = useState("");
  const [step, setStep] = useState<SupplyStep>("idle");
  const [error, setError] = useState<string | null>(null);
  const [statusMsg, setStatusMsg] = useState("");
  const [txHash, setTxHash] = useState<string | null>(null);
  const [isExecuting, setIsExecuting] = useState(false);
  const [, setShowStatusPanel] = useState(false);
  const [, setPaymentResult] = useState<any>(null);
  const submittedRef = useRef(false);
  const abortRef = useRef<AbortController | null>(null);

  const selectedAsset = selected ? { asset: selected, amount } : null;

  // Routing quote: does the chosen source asset need a swap into the destination token?
  const quote: SupplyQuoteResult = useSupplyQuote({
    direction: "input",
    selectedAsset,
    destinationToken,
    chainId: destinationToken.chainId,
    recipient: embeddedWalletAddress ?? address,
    paymentAmount: amount,
  });

  // The token the execution is funded in on the destination chain
  const executionTokenAddress: Address =
    quote.needsSwap && quote.resolvedTokenIn
      ? quote.resolvedTokenIn
      : selected && selected.chainId === destinationToken.chainId
        ? selected.address
        : (quote.resolvedTokenIn ?? destinationToken.address);

  const isSourceDestToken =
    !!selected &&
    selected.address.toLowerCase() === destinationToken.address.toLowerCase() &&
    selected.chainId === destinationToken.chainId;

  const supply = async () => {
    if (!selectedAsset) return;

    const batchCtx: BatchBuilderContext = {
      currentPaymentAmount: amount,
      destinationToken,
      embeddedWalletAddress,
      providerEmbeddedAddress: embeddedWalletAddress,
      externalWalletAddress: externalAddress,
      address,
      isNon7702,
      isPayMode: false,
      resolvedChainId: destinationToken.chainId,
      buildActionCalls,
      feeExecutionMode: "backend-transfer",
      buildSpicenetBatch: undefined,
      selectedAsset,
      needsSwap: quote.needsSwap,
      resolvedSwap: quote.resolvedSwap,
      exactOutputSwap: quote.exactOutputSwap,
      quoteAmountOut: quote.quoteAmountOut,
      resolvedTokenIn: quote.resolvedTokenIn,
      requestTimestampMs: Date.now(),
    };
    const buildSupply = (opts?: Parameters<typeof buildSupplyChainBatches>[1]) =>
      buildSupplyChainBatches(batchCtx, opts);
    const buildPay = (opts?: Parameters<typeof buildPayChainBatches>[1]) =>
      buildPayChainBatches(batchCtx, opts);

    const parsedAmount = parseFloat(amount) || 0;

    // Fee quote. In production, run this when the amount settles and show it
    // before enabling the submit button.
    const feeQuote = await estimateFeePreview({
      selectedAsset,
      isPayMode: false,
      parsedAmount,
      needsSwap: quote.needsSwap,
      exactOutputSwap: quote.exactOutputSwap,
      isNon7702,
      isSourceDestToken,
      currentPaymentAmount: amount,
      destinationToken,
      feeExecutionMode: "backend-transfer",
      resolvedTokenIn: quote.resolvedTokenIn,
      embeddedWalletAddress,
      providerEmbeddedAddress: embeddedWalletAddress,
      resolvedChainId: destinationToken.chainId,
      isConnected,
      address,
      ready: isReady,
      authenticated: isAuthenticated,
      isDirect: quote.isDirect,
      resolvedSwap: quote.resolvedSwap,
      isQuoting: quote.isQuoting,
      executionTokenAddress,
      buildPayChainBatches: buildPay,
      buildSupplyChainBatches: buildSupply,
      estimate,
    });

    const ctx: ExecutorContext = {
      selectedAsset,
      parsedAmount,
      currentPaymentAmount: amount,
      isInsufficientBalance: parsedAmount > (selected?.balanceFormatted ?? 0),
      isPayMode: false,
      isNon7702,
      isSourceDestToken,
      needsSwap: quote.needsSwap,
      exactOutputSwap: quote.exactOutputSwap,
      selectedIsEquivalent: quote.selectedIsEquivalent,
      resolvedTokenIn: quote.resolvedTokenIn,
      embeddedWalletAddress,
      providerEmbeddedAddress: embeddedWalletAddress,
      externalWallet: null,
      externalWalletAddress: externalAddress,
      address,
      isConnected,
      ready: isReady,
      authenticated: isAuthenticated,
      provider,
      resolvedChainId: destinationToken.chainId,
      executionTokenAddress,
      feeQuote,
      execution: {
        estimate,
        execute,
        buildPayChainBatches: buildPay,
        buildSupplyChainBatches: buildSupply,
        onProgress: (p) => setStatusMsg(p.message),
      },
      ui: {
        setError,
        setStep,
        setStatusMsg,
        setTxHash,
        setIsExecuting,
        setShowStatusPanel,
        setPaymentResult,
      },
      refs: {
        spiceDepositSubmittedRef: submittedRef,
        abortControllerRef: abortRef,
      },
      destinationToken,
      buildActionCalls,
      feeExecutionMode: "backend-transfer",
      buildSpicenetBatch: undefined,
      onPayExecute: undefined,
      onSuccess: (hash) => console.log("supplied", hash),
    };

    await executeSupply(ctx);
  };

  return (
    <div>
      {displayAssets.map((a) => (
        <button key={`${a.chainId}-${a.address}`} onClick={() => setSelected(a)}>
          {a.symbol} on chain {a.chainId}: {a.balanceFormatted}
        </button>
      ))}
      <input value={amount} onChange={(e) => setAmount(e.target.value)} />
      <button onClick={supply} disabled={isExecuting || !selected}>
        {step === "success" ? "Done" : "Supply"}
      </button>
      {statusMsg && <p>{statusMsg}</p>}
      {error && <p>{error}</p>}
      {txHash && <p>{txHash}</p>}
    </div>
  );
}
```

Everything above the JSX is wiring; the JSX is yours to replace entirely.

## Scaling to All Markets

Only two things vary per market: `destinationToken` and `buildActionCalls`. Wrap the panel in a
component that takes those as props (or a market config object) and every market gets any-asset,
any-chain funding with the same code path.

## Withdrawals

`SpiceWithdraw` handles withdrawing Spice balances back to a wallet on any supported chain. To build
your own withdraw UI instead, use `useSpiceAssets` for balances and `useSpiceExecution` for
execution — see [Hooks](/sdk/hooks).

## Testing

Set `network="testnet"` on `SpiceFlowProvider` (and swap the wagmi/Privy chain set to the testnet
chains) to run the identical flow against testnet infrastructure. Testnet funds for supported tokens
come from the [airdrop endpoint](/api-reference/endpoint/airdrop). Move to `network="mainnet"` only
after the full supply → withdraw loop passes on testnet.

## Notes

* `feeExecutionMode: "backend-transfer"` lets the backend collect fees from the funded amount; the
  fee quote's `netAmount` is what reaches the market.
* `executeSupply` drives your state through the `ui` setters — render `step`, `statusMsg`, and
  `error` however you like. Steps: `idle → signing-delegation → signing-intent → submitting →
  executing → success | error`.
* Supported chains come from the SDK per `network` — see
  [Configuration](/sdk/configuration#default-chains-by-network). Restrict with `supportedChainIds`
  or `sourceChains` on `useSupplyAssets`.
