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

# Build custom deposit UI with the headless core

> Use Trustware routing, wallet plumbing, transaction orchestration, and settlement automations without any widget UI by calling the Trustware core API directly.

The headless core gives you Trustware's routing and transaction logic without the prebuilt widget. You build your own UI and the SDK handles route construction, quotes, wallet calls, settlement, and transaction submission.

## When to use this pattern

Choose this pattern when:

* you want full control over the deposit UI
* your design system requires a custom amount entry or confirmation flow
* you want to embed deposit logic into an existing interface without a floating widget
* you need a flow the widget does not provide, such as [withdrawing from an embedded wallet](/guides/embedded-wallet-withdrawals)
* you need to deposit into a contract on the destination chain, such as [depositing into a vault destination](/guides/vault-destinations)

If you want built-in wallet selection, token picker, amount entry, and confirmation screens, use the [drop-in widget](/integration/drop-in-widget) instead.

## Setup

The headless core still requires `TrustwareProvider` for config context. Mount it once near the root of your app; you do not need to render `TrustwareWidget`.

```tsx theme={null}
import { TrustwareProvider, type TrustwareConfigOptions } from "@trustware/sdk";

const trustwareConfig = {
  apiKey: process.env.NEXT_PUBLIC_TRUSTWARE_API_KEY!,
  routes: {
    toChain: "8453",
    toToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
    defaultSlippage: 1,
    options: {
      routeRefreshMs: 15000,
    },
  },
} satisfies TrustwareConfigOptions;

export function App() {
  return (
    <TrustwareProvider config={trustwareConfig}>
      {/* your custom UI goes here */}
    </TrustwareProvider>
  );
}
```

Then import the core in any component or hook:

```ts theme={null}
import { Trustware } from "@trustware/sdk";
```

## Wallet setup

You have two options for attaching a wallet to the headless core.

### Bring your own wallet

Adapt an existing Wagmi wallet client into Trustware using `useWallet`:

```ts theme={null}
import { useEffect } from "react";
import { useWalletClient } from "wagmi";
import { useWagmi } from "@trustware/sdk/wallet";
import { Trustware } from "@trustware/sdk";

export function useTrustwareWalletBridge() {
  const { data } = useWalletClient();

  useEffect(() => {
    if (!data) return;
    Trustware.useWallet(useWagmi(data));
  }, [data]);
}
```

### Let Trustware detect wallets

If you do not manage wallet state, call `autoDetect` once at startup:

```ts theme={null}
await Trustware.autoDetect();
```

## Core operations

### 1. Build a route

`buildRoute` constructs a route. Use this when you want to inspect route details before asking the user to confirm.

```ts theme={null}
const route = await Trustware.buildRoute({
  fromChain: "1",
  toChain: "8453",
  fromToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  toToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
  fromAmount: "1000000", // amount in token's smallest unit
  fromAddress: await Trustware.getAddress(),
  toAddress: "0xDestination...",
});
```

**`BuildRouteBody` shape:** (`?` indicates optional)

```ts theme={null}
export type BuildRouteBody = {
  fromChain: string;
  toChain: string;
  fromToken: string;
  toToken: string;
  fromAmount: string;
  fromAddress: string;
  toAddress: string;
  fromAmountUsd?: string;
  fromAmountUSD?: string;
  refundAddress?: string;
  direction?: string;
  slippage?: number;
  slippageBps?: number;
  memo?: string;
  hooks?: { postHook?: PostHookRequest };
};
```

`hooks.postHook` executes a contract call on the destination chain once routed funds arrive, instead of paying the destination token to `toAddress`. It requires SDK 1.1.10 or later, is fully optional, and omitting `hooks` leaves route behavior unchanged. See [`PostHookRequest`](/api-reference/types#posthookrequest) for the field reference and [vault destinations](/guides/vault-destinations) for the end-to-end flow.

### 2. Inspect route details

The returned `BuildRouteResult` contains exchange rate information you can display to the user before they confirm.

```ts theme={null}
const route = await Trustware.buildRoute({
  fromChain: "1",
  toChain: "8453",
  fromToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  toToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
  fromAmount: "1000000",
  fromAddress: await Trustware.getAddress(),
  toAddress: "0xDestination...",
});

// Show the user what they will receive before confirming
console.log(route.finalExchangeRate.fromAmountUSD);
console.log(route.finalExchangeRate.toAmountMinUSD);
```

**`BuildRouteResult` shape:**

```ts theme={null}
type BuildRouteResult = {
  intentId: string;              // pass to receipt and status calls
  txReq: TxRequest;              // transaction object to sign and broadcast
  actions: unknown[];
  finalExchangeRate: {
    fromAmountUSD?: string;
    toAmountMinUSD?: string;
  };
  route: RoutePlan | undefined;
  sponsorship?: RouteSponsorship; // present when the route is gas sponsored
};
```

The `txReq` type is not exported under its own name. Refer to it as `BuildRouteResult["txReq"]` rather than importing a named type.

### 3. Sign and send a route

`sendRouteTransaction` switches the wallet to the route's chain if needed, grants any ERC-20 allowance the route requires, and then sends the route transaction. It returns the source transaction hash.

```ts theme={null}
const txHash = await Trustware.sendRouteTransaction(route, 1);
await Trustware.submitReceipt(route.intentId, txHash);

const result = await Trustware.pollStatus(route.intentId);

if (result.status === "success") {
  console.log("Deposit complete:", result.destTxHash);
}
```

On EVM routes, allowance handling is automatic. When the route response includes `route.execution.approvals`, the SDK reads the current allowance, submits an approval for the exact amount when one is missing, waits for that approval to confirm, and only then requests the route signature.

Plan your UI around two consequences:

* The user can see more than one wallet prompt for a single deposit. An ERC-20 source asset usually means an approval prompt followed by the deposit prompt.
* Each approval must confirm on chain before the route signature is requested, so the call can stay pending for a minute or more.

Every approval the route lists is for the exact amount required, never an unlimited allowance. An ordinary EOA route processes all of them. If you sign transactions yourself, process `route.execution.approvals` in the same order: read, approve, wait, then send.

<Warning>
  A route carrying valid gas sponsorship is the exception. `sendRouteTransaction`
  skips the listed approvals in that case, because the sponsored path handles the
  allowance itself: it grants the bridge allowance inside the user operation, and
  it pulls the source tokens through a one-time unlimited Permit2 approval that it
  requests separately. Neither is a `route.execution.approvals` entry. That path is
  smart-account execution, so do not pass a sponsored route to
  `sendRouteTransaction` with a standard EIP-1193 wallet: it sends a plain
  transaction and the approval is skipped with nothing to replace it. Use
  `sendRouteAsUserOperation()` instead, as [Paymasters](/guides/paymasters)
  describes.
</Warning>

### 4. Run the full flow

`runTopUp` handles route construction, wallet approval, transaction submission, and status polling in a single call. Use this when you want the SDK to orchestrate the full deposit path.

Only `fromAmount` is required; every other field is optional and falls back to the corresponding value in your `TrustwareConfigOptions.routes` config when omitted.

```ts theme={null}
const result = await Trustware.runTopUp({
  fromAmount: "1000000",                                       // required: amount in token's smallest unit
  fromChain: "1",                                              // optional: overrides config.routes.fromChain
  toChain: "8453",                                             // optional: overrides config.routes.toChain
  fromToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",     // optional: overrides config.routes.fromToken
  toToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",       // optional: overrides config.routes.toToken
  toAddress: "0xDestination...",                               // optional: overrides config.routes.toAddress
});

if (result.status === "success") {
  console.log("Deposit complete:", result.destTxHash);
}
```

**Full signature:**

```ts theme={null}
Trustware.runTopUp(params: {
  fromAmount: string | number;  // required
  fromChain?: string;            // overrides config.routes.fromChain
  toChain?: string;              // overrides config.routes.toChain
  fromToken?: string;            // overrides config.routes.fromToken
  toToken?: string;              // overrides config.routes.toToken
  toAddress?: string;            // overrides config.routes.toAddress
});
```

## Working with chains and tokens

The same chain and token discovery the widget uses internally is available to your own UI through three helpers on the `Trustware` facade. All three respect the active `TrustwareProvider` config and reuse its cache.

### `Trustware.useChains()`

React hook that returns the supported chain list, split into popular and other groups, with loading and error state. Use it to drive your own chain selector.

```tsx theme={null}
import { Trustware } from "@trustware/sdk";

const useTrustwareChains = Trustware.useChains;

function ChainPicker() {
  const { popularChains, otherChains, chains, isLoading, error } =
    useTrustwareChains();

  if (isLoading) return <Spinner />;
  if (error) return <ErrorState message={error} />;
  return <ChainList chains={[...popularChains, ...otherChains]} />;
}
```

### `Trustware.useTokens(chainId)`

React hook that returns the token list for a given chain with built-in pagination and search. Pass `null` to skip fetching.

```tsx theme={null}
const {
  filteredTokens,
  hasNextPage,
  isLoading,
  isLoadingMore,
  error,
  loadMore,
  searchQuery,
  setSearchQuery,
} = Trustware.useTokens(activeChain?.chainId ?? null);
```

### `Trustware.validateAddressForChain(address, chain)`

Synchronous helper that returns `{ isValid: boolean; error?: string }` for the given destination address against the rules of the selected chain (EVM checksum, Solana base58, Bitcoin, Cosmos prefixes, etc.). Use it for live form validation.

```ts theme={null}
const validation = Trustware.validateAddressForChain(
  destinationAddress,
  activeChain,
);

if (!validation.isValid) {
  showError(validation.error);
}
```

## Scanning wallet balances

Three helpers read balances. `Trustware.getBalances(chainRef, address)` covers a single chain and is the one to use when you already know which chain you care about, as in [embedded wallet withdrawals](/guides/embedded-wallet-withdrawals). The other two scan every chain compatible with the address format.

### `Trustware.getBalancesByAddress(address, options)`

Resolves once with one `WalletAddressBalanceWrapper` per chain. Pass `stream: true` to have the SDK consume the streamed scan and merge the chunks for you. That changes the transport, not the timing: the promise still settles after the last chain reports, so use `getBalancesByAddressStream` below if you want to render progressively.

```ts theme={null}
const wrappers = await Trustware.getBalancesByAddress(address, {
  stream: true,
  onSummary: ({ partial }) => {
    if (partial) setNotice("Some chains did not report. Balances may be incomplete.");
  },
});
```

Read `partial` before you tell a user their wallet is empty: a chain that timed out and a chain with no holdings both come back as no rows. See [`BalanceStreamSummary`](/api-reference/types#balancestreamsummary) for when the callback fires and what each field means.

### `Trustware.getBalancesByAddressStream(address, options)`

Async generator that yields each chain's balances as it lands, so you can paint a wallet that fills in rather than a spinner that sits.

```ts theme={null}
import { Trustware, type WalletAddressBalanceWrapper } from "@trustware/sdk";

const byChain = new Map<string, WalletAddressBalanceWrapper>();

for await (const chunk of Trustware.getBalancesByAddressStream(address, {
  signal: controller.signal,
  onSummary: ({ partial }) => setPartial(partial),
})) {
  for (const wrapper of chunk) {
    byChain.set(wrapper.chain_id, wrapper);
  }
  setBalances([...byChain.values()]);
}
```

Each yielded chunk holds only the chains that just reported, and chains report in completion order rather than list order, so merge into a map keyed by `chain_id` instead of appending.

Aborting `signal` does not end the scan. The abort surfaces as a stream failure, so the SDK falls back and re-runs the scan against the buffered endpoint, which the signal does not reach. Stop consuming the generator if you need the results dropped.

<Note>
  Streaming is on by default from SDK 1.1.12 through `features.balanceStreaming`.
  Where a backend has streaming disabled, the request is aborted, or the runtime
  has no readable response body (React Native, most notably), the generator emits
  `balance_stream_fallback` and yields the buffered result as a single chunk.
  Setting the flag to `false` does the same thing without the event. Either way
  this code path still works, so it does not need a variant. See
  [`BalanceStreamOptions`](/api-reference/types#balancestreamoptions) and
  [`GET /balances`](/api-reference/balances#streaming-responses).
</Note>

## Lifecycle callbacks

`TrustwareConfigOptions` exposes three optional callbacks you can pass alongside your routes config to react to SDK activity:

```ts theme={null}
onError?: (error: TrustwareError) => void;
onSuccess?: (transaction: Transaction) => void;
onEvent?: (event: TrustwareEvent) => void;
```

* **`onEvent`** fires for every lifecycle event the SDK emits. It receives the full `TrustwareEvent` discriminated union, which you can narrow by `type` (`transaction_started`, `transaction_success`, `wallet_connected`, etc.).
* **`onSuccess`** fires once when a deposit settles on the destination chain. It receives the resolved `Transaction` with `destTxHash` populated.
* **`onError`** fires for any `TrustwareError` thrown during a route or transaction operation. Use it as a single place to log errors or push them into your UI state. `RateLimitError` extends native `Error` (not `TrustwareError`) and does not flow through this callback; handle it with the imperative `try/catch` pattern below.

```ts theme={null}
const config = {
  apiKey: process.env.NEXT_PUBLIC_TRUSTWARE_API_KEY!,
  routes: {
    toChain: "8453",
    toToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
  },
  onEvent: (event) => {
    if (event.type === "transaction_success") {
      console.log("TX hash:", event.txHash);
    }
    if (event.type === "wallet_connected") {
      console.log("Wallet:", event.address);
    }
  },
  onSuccess: (transaction) => {
    console.log("Deposit complete:", transaction.destTxHash);
  },
  onError: (error) => {
    console.error("Trustware error:", error.code, error.message);
  },
} satisfies TrustwareConfigOptions;
```

`onError` runs in addition to any `try/catch` you wrap around individual core calls; see [Error handling](#error-handling) below for the imperative pattern. See [lifecycle events](/events-errors/lifecycle-events) for the full list of event types.

## Error handling

Wrap core calls in try/catch. Import `RateLimitError` to handle rate-limiting specifically:

```ts theme={null}
import { RateLimitError, Trustware } from "@trustware/sdk";

try {
  const route = await Trustware.buildRoute({
    fromChain: "1",
    toChain: "8453",
    fromToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    toToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
    fromAmount: "1000000",
    fromAddress: await Trustware.getAddress(),
    toAddress: "0xDestination...",
  });
} catch (error) {
  if (error instanceof RateLimitError) {
    console.error("Rate limited", error.rateLimitInfo, error.retriesExhausted);
  }
}
```

The SDK already waited out the limit on the server's own schedule before throwing, so do not retry immediately. See [Rate limiting and retry configuration](/configuration/retry) for what the two `retriesExhausted` cases mean.

## When to use the widget instead

<Tip>
  If you want built-in wallet selection, token picker, amount input, and processing/success screens, the widget patterns require significantly less code. The headless core is best when your UI requirements cannot be met by the prebuilt widget.
</Tip>

| You want | Use |
| - | - |
| Full built-in UX | [Drop-in widget](/integration/drop-in-widget) |
| Widget with your own wallet | [Host wallet](/integration/host-wallet) |
| Programmatic open/close | [Controlled widget](/integration/controlled-widget) |
| Custom UI, SDK routing | Headless core (this page) |


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