# SilentSwap
> Integrate private and public cross-chain swaps with the SilentSwap TypeScript SDK.
## For AI agents
Use the machine-readable artifacts instead of scraping rendered HTML:
* [llms.txt](https://docs.silentswap.com/llms.txt) — page index with descriptions.
* [llms-full.txt](https://docs.silentswap.com/llms-full.txt) — all documentation in one file.
* [SilentSwap integration skill](https://docs.silentswap.com/skill/SKILL.md) — operational integration procedure.
* [Complete skill example](https://docs.silentswap.com/skill/references/complete-example.ts) — compiling EVM flow.
* [Raw GitHub skill](https://raw.githubusercontent.com/SilentSwap-V3/silentswap-sdk/main/skills/silentswap-integration/SKILL.md).
### Install the skill
```sh
mkdir -p .claude/skills/silentswap-integration/references
curl -fsSL https://docs.silentswap.com/skill/SKILL.md \
-o .claude/skills/silentswap-integration/SKILL.md
curl -fsSL https://docs.silentswap.com/skill/references/complete-example.ts \
-o .claude/skills/silentswap-integration/references/complete-example.ts
```
For Codex, place the same folder under .agents/skills/silentswap-integration.
### Three invariants
1. Render every output, amount, fee, and expiry before calling placeOrder(quote).
V3 has no authorization-signing or confirmation ceremony.
2. Production baseUrl defaults to [https://api.silentswap.com](https://api.silentswap.com).
Override it only for tests or self-hosted deployments.
3. Treat COMPLETED, FAILED, and ABORTED as the statuses
that close the SDK watcher. DROPPED is resumable when a late deposit lands, so
keep watching it. Always release the unsubscribe function when its owner unmounts.
### Type-check the real package
```ts twoslash
import { createSilentSwapClient, type PrivateQuote } from '@silentswap/sdk';
const client = createSilentSwapClient();
declare const quote: PrivateQuote;
client.placeOrder(quote);
```
## Getting started
SilentSwap V3 exposes one small, typed flow: quote, show the user what will happen, place,
and track.
### Install
```sh
npm install @silentswap/sdk viem
```
### Create a client
Production uses [https://api.silentswap.com](https://api.silentswap.com) automatically. Supply an EVM wallet
client and any narrow non-EVM capability adapters the application already selected.
```ts twoslash
import { createSilentSwapClient } from '@silentswap/sdk';
const client = createSilentSwapClient();
```
### Follow the trust boundary
The wallet deposit calldata does not reveal the recipient. Render every output, asset, fee,
and expiry in your own trusted UI, then call placeOrder(quote). There is no V2
authorization-signing replacement.
Continue with the [quickstart](/sdk/quickstart), or read [how it works](/how-it-works) before
handling funds.
## How it works
SilentSwap separates what the user reviews from what the wallet signs.
### 1. Quote
quote sends the source asset, source chain, and one or more outputs to the
SilentSwap API. The response contains a ticket, fees, expiry, destination previews, and the
deposit payload.
### 2. Review and place
Show every output, asset, fee, and expiry. Then place the reviewed quote directly:
```ts twoslash
import type { PrivateQuote, SilentSwapClient } from '@silentswap/sdk';
declare const client: SilentSwapClient;
declare const quote: PrivateQuote;
const order = await client.placeOrder(quote);
order.reference;
```
V3 has no nonce, SIWE, facilitator, or authorization-signing ceremony.
### 3. Deposit
For EVM sources, the SDK checks allowance, simulates the deposit, sends it, and waits for a
receipt. Bitcoin, Litecoin, Solana, TON, and TRON payloads branch to their native wallet flow;
see [deposits](/sdk/deposits).
### 4. Fulfill and claim
The backend observes the deposit, privately fulfills every recipient, obtains the notary
signature, and claims the gateway deposit. Your integration follows the ticket with
trackOrderViaWebSocket rather than reconstructing this orchestration.
### Security invariants
* Never place a quote before trusted UI displays its outputs, assets, fees, and expiry.
* Never replace quoted calldata or destination metadata.
* Never merge addresses across wallet brands.
* Never hardcode registry addresses, token decimals, quote amounts, or fees.
## React hooks — coming soon
@silentswap/react 2.x will provide hooks over the SDK's canonical client. It is
not published with the 2.0.0 SDK release.
Use the framework-independent client inside your current wallet and data layer:
```ts twoslash
import {
createSilentSwapClient,
type SilentSwapClient,
} from '@silentswap/sdk';
import { bsc } from 'viem/chains';
export const silentSwap: SilentSwapClient = createSilentSwapClient({
baseUrl: 'https://api.silentswap.com',
chain: bsc,
});
```
Do not install a V2 React, Vue, widget, or UI-kit package into a V3 integration. Their
authentication and order models target the discontinued V2 backend.
## Migrate from SilentSwap V2
@silentswap/sdk\@2.0.0 keeps the package name and familiar lifecycle names, but
removes the V2 authentication and authorization ceremony. Existing V2 orders are not
convertible: let them finish under your pinned 0.x integration before cutting over.
### Migrate with an agent
Install the [SilentSwap integration skill](https://docs.silentswap.com/skill/SKILL.md) and let an agent perform the
mechanical audit and rewrite. The skill preserves application-specific wallet decisions as
explicit TODOs instead of guessing a wallet brand.
```sh
mkdir -p .agents/skills/silentswap-integration/references
curl -fsSL https://docs.silentswap.com/skill/SKILL.md \
-o .agents/skills/silentswap-integration/SKILL.md
curl -fsSL https://docs.silentswap.com/skill/references/complete-example.ts \
-o .agents/skills/silentswap-integration/references/complete-example.ts
```
Use this prompt:
```text
Use $silentswap-integration to migrate this application from @silentswap/sdk 0.x to 2.x.
Audit every old import and call site; remove nonce, SIWE, facilitator, and authorization
persistence; migrate quote, placeOrder, trackOrderViaWebSocket, executeRefund, errors, and
client configuration; flag pending V2 orders that must finish on pinned 0.x; run the
consumer typecheck; and leave TODOs only where this app must choose its wallet adapters.
```
For Claude Code, install the same folder under
.claude/skills/silentswap-integration.
### API mapping
| V2 | V3 |
| -------------------------------------------- | ---------------------------------------------------------------------- |
| createSilentSwapClient | unchanged |
| nonce/SIWE authentication | removed |
| facilitator group | removed |
| quote(\{ outputs, pro }) | quote(\{ privacy, inputAddress, outputs, integratorFee }) |
| sign authorizations | removed; review the quote, then deposit |
| order() | placeOrder(quote) |
| trackOrderViaWebSocket | same name; transport is adaptive |
| executeRefund | same name; accepts a private order reference |
| Simple Bridge | same product name; use privacy: false |
| pro / integratorId | integratorFee / integratorAddress |
| React/Vue/widget packages | discontinued; React 2.x is planned |
Do not mechanically translate the old pro integrator ID into an integrator fee. Only add
integratorFee and integratorAddress after the application owner has
approved the USDC-denominated fee and payout address.
#### Rewrite each output
V2 and V3 both call the array outputs, but each row is simpler in V3:
| V2 output field | V3 output field |
| -------------------------------------- | ------------------------------------------------------------------------- |
| recipient | address |
| USDC decimal-string value | bigint amount in USDC base units |
| non-USDC value | recompute the V3 USDC payout budget; do not copy destination-native units |
| CAIP-19 asset | derive destination chainId and dest only |
| method | removed |
| facilitatorPublicKeys | removed |
| extra.swap | optional dest, when it names an arbitrary destination asset |
Derive top-level sourceAsset or sourceToken independently from the old
application's source selection/deposit flow. Review ambiguous CAIP-19 or
extra.swap values with the application owner; an agent should leave a focused TODO
instead of guessing chain or token metadata.
### Revised order flow
```ts twoslash
import { createSilentSwapClient } from '@silentswap/sdk';
import { parseUnits } from 'viem';
import type { WalletClient } from 'viem';
declare const account: `0x${string}`;
declare const recipient: `0x${string}`;
declare const walletClient: WalletClient;
const client = createSilentSwapClient({ walletClient });
const quote = await client.quote({
privacy: true,
inputAddress: account,
sourceChainId: 1,
sourceAsset: 'USDC',
outputs: [
{
address: recipient,
chainId: 8453,
amount: parseUnits('10', 6),
},
],
});
// Render quote.outputs/recipients, amounts, assets, fees, and expiresAt here.
const order = await client.placeOrder(quote);
const stop = client.trackOrderViaWebSocket(order.reference, console.log, console.error);
void stop;
```
There is no replacement authorization ceremony. The user reviews the RFQ in your trusted UI,
then placeOrder handles an ERC-20 approval when necessary and submits the deposit.
Unlike V2, tracking no longer returns a promise that resolves to the final status: it emits
states through callbacks and immediately returns the teardown function. Refunds now return
\{ refundTxHash } from
client.executeRefund(privateReference).
### Behavioral changes
* V3 throws typed errors instead of returning \[error, value] tuples.
* Production baseUrl is optional and defaults to
[https://api.silentswap.com](https://api.silentswap.com).
* Public request terminology remains outputs\[]; the SDK maps it to the private
backend's unchanged recipients\[] wire field.
* privacy: true always requires inputAddress.
* privacy: false uses Simple Bridge with the same quote/place/track lifecycle.
* Integrator fees use USDC base units and require integratorAddress when positive.
* Browser integrations must register their exact HTTPS production origin with SilentSwap
support. Node and server integrations do not need CORS registration.
Remove persisted V2 nonces, SIWE tokens, facilitator wallets, authorization signatures, and
WebSocket recovery data. Do not reinterpret them as V3 credentials.
## Complete Simple Bridge example
```ts twoslash
import { SimpleBridgeRevertedError, createSilentSwapClient } from '@silentswap/sdk';
import type { WalletClient } from 'viem';
declare const walletClient: WalletClient;
const client = createSilentSwapClient({ walletClient });
export async function startSimpleBridge(): Promise<() => void> {
const quote = await client.quote({
privacy: false,
inputAddress: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
sourceChainId: 8453,
sourceAsset: 'USDC',
amount: 25_000_000n,
outputs: [
{
address: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
chainId: 1,
dest: {
kind: 'evm',
chainId: 1,
assetId: '0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
assetSymbol: 'USDC',
assetDecimals: 6,
},
},
],
});
try {
const order = await client.placeOrder(quote);
return client.trackOrderViaWebSocket(
order.reference,
(state) => console.log(state.status),
console.error,
);
} catch (error) {
if (error instanceof SimpleBridgeRevertedError) {
throw new Error(`Simple Bridge reverted: ${error.txHash}`, { cause: error });
}
throw error;
}
}
```
## Simple Bridge
Simple Bridge is the non-private mode of the same client. Set privacy: false and
use the same quote → placeOrder → trackOrderViaWebSocket lifecycle. The SDK
normalizes bridge states into FULFILLING, COMPLETED,
FAILED, or UNKNOWN.
```ts twoslash
import { createSilentSwapClient } from '@silentswap/sdk';
import type { WalletClient } from 'viem';
declare const walletClient: WalletClient;
const client = createSilentSwapClient({
walletClient,
});
```
Quotes and tracking use the configured SilentSwap API automatically. Integrations never need
to configure or depend on an underlying routing provider.
## Simple Bridge usage
```ts twoslash
import { createSilentSwapClient } from '@silentswap/sdk';
import type { WalletClient } from 'viem';
declare const walletClient: WalletClient;
const client = createSilentSwapClient({ walletClient });
const quote = await client.quote({
privacy: false,
inputAddress: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
sourceChainId: 1,
sourceAsset: 'ETH',
amount: 10_000_000_000_000_000n,
outputs: [
{
address: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
chainId: 8453,
dest: {
kind: 'evm',
chainId: 8453,
assetId: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
assetSymbol: 'USDC',
assetDecimals: 6,
},
},
],
});
const order = await client.placeOrder(quote);
const stop = client.trackOrderViaWebSocket(order.reference, console.log, console.error);
void stop;
```
Simple Bridge is exact-input and accepts exactly one output. Integrator fees are not currently
available for public routes.
## Assets and chains
Install the registries directly when an application needs picker or routing metadata without
the full SDK.
```ts twoslash
import {
getAssetOnChain,
listAssets,
} from '@silentswap/assets';
import {
getChainConfig,
getSourceChain,
listSourceChains,
} from '@silentswap/chains';
import { ASSETS } from '@silentswap/sdk';
const baseUsdc = getAssetOnChain('USDC', 8453);
const base = getChainConfig(8453);
const bnbSource = getSourceChain(56);
const sources = listSourceChains();
console.log(ASSETS, listAssets(), baseUsdc, base, bnbSource, sources);
```
Use the registry's token address and decimals as one pair. Do not hardcode them or assume USDC
uses six decimals on every chain. Use NATIVE\_TOKEN\_SENTINEL to detect native gas
assets.
viem is a peer dependency of both public registry packages, preventing duplicate
Chain type identities.
## Client API
### Configuration
```ts twoslash
import type { SilentSwapConfig } from '@silentswap/sdk';
type ConfigKeys = keyof SilentSwapConfig;
// ^?
```
| Field | Required | Purpose |
| --------------------------- | -------------------- | ---------------------------------------------------------------------------------- |
| baseUrl | No | Defaults to [https://api.silentswap.com](https://api.silentswap.com). |
| chain | No | Defaults to the configured production gateway chain. |
| walletClient | Wallet calls | Deposits, allowances, and refunds. |
| walletAdapters | Non-EVM wallet calls | Narrow Solana, Bitcoin, TON, and TRON capabilities. |
| publicClient | No | Read client for gateway allowance/receipt work. |
| rpcUrlForChain | No | Resolve read RPCs for source chain IDs. |
### Methods
| Method | Return |
| ----------------------------------------------------------------- | --------------------------------------------------------------------------- |
| quote(request) | Promise\ |
| hasAllowance(quote) | Promise\ |
| ensureAllowance(quote) | Approval hash and sufficiency |
| previewDepositCost(quote) | Native-gas cost breakdown |
| hasSufficientGasForDeposit(quote) | Balance/cost comparison |
| placeOrder(quote) | Serializable reference, transaction hashes, or external pay-in instructions |
| getOrder(privateReference) | Promise\ |
| trackOrderViaWebSocket(reference, onState, onError?) | Unsubscribe function |
| executeRefund(privateReference) | Refund transaction hash |
The class SilentSwap, factory createSilentSwapClient, and type alias
SilentSwapClient refer to the same canonical client surface.
## Complete private-swap example
This example shows the full EVM path. The UI must render the quote before calling
placeOrder.
```ts twoslash
import {
AmountOutOfRangeError,
DepositSimulationError,
HttpError,
createSilentSwapClient,
} from '@silentswap/sdk';
import {
createWalletClient,
custom,
parseUnits,
type Address,
} from 'viem';
import { mainnet } from 'viem/chains';
declare const provider: Parameters[0];
declare const account: Address;
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: custom(provider),
});
const client = createSilentSwapClient({
walletClient,
});
export async function startSwap(): Promise<() => void> {
try {
const quote = await client.quote({
privacy: true,
inputAddress: account,
sourceAsset: 'USDC',
sourceChainId: 1,
outputs: [{
address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
chainId: 8453,
amount: parseUnits('25', 6),
}],
});
// Await a real UI review here, then optionally narrate approval.
if (!(await client.hasAllowance(quote))) {
await client.ensureAllowance(quote);
}
const order = await client.placeOrder(quote);
return client.trackOrderViaWebSocket(
order.reference,
(state) => console.log(state.status),
console.error,
);
} catch (error) {
if (error instanceof AmountOutOfRangeError) {
throw new Error(error.message, { cause: error });
}
if (error instanceof DepositSimulationError) {
throw new Error('Nothing was broadcast.', { cause: error });
}
if (error instanceof HttpError) {
throw new Error(`SilentSwap API: HTTP ${error.status}`, { cause: error });
}
throw error;
}
}
```
## Errors
The SDK throws errors. It does not return V2-style \[error, value] tuples.
```ts twoslash
import {
AmountOutOfRangeError,
DepositRevertedError,
DepositSimulationError,
HttpError,
MaintenanceModeError,
RefundNotAllowedError,
RefundRevertedError,
} from '@silentswap/sdk';
export function describeSilentSwapError(error: unknown): string {
if (error instanceof MaintenanceModeError) return error.message;
if (error instanceof AmountOutOfRangeError) {
return `${error.code}: ${error.limitAmount ?? 'unknown limit'}`;
}
if (error instanceof DepositSimulationError) return error.message;
if (error instanceof DepositRevertedError) return `Reverted: ${error.depositTxHash}`;
if (error instanceof RefundNotAllowedError) return error.message;
if (error instanceof RefundRevertedError) return `Reverted: ${error.refundTxHash}`;
if (error instanceof HttpError) return `HTTP ${error.status}`;
return error instanceof Error ? error.message : 'Unknown SilentSwap error';
}
```
A DepositSimulationError means no transaction was sent.
DepositRevertedError and RefundRevertedError preserve hashes for
explorer links. HttpError.body carries the API error payload.
Detect wallet user rejection separately from protocol failures so a cancelled signature is not
reported as an API incident.
## Installation
Install the SDK and viem together:
```sh
npm install @silentswap/sdk viem
```
The public package graph is @silentswap/sdk → @silentswap/assets →
@silentswap/chains. All three packages ship at version 2.0.0. They share
viem ^2.21.55 as a peer to preserve one Chain type identity.
### Runtime support
The package is ESM and exports JavaScript, declarations, source maps, and TypeScript source.
Use a modern browser or current Node.js runtime with fetch. Browser tracking uses
EventSource; Node.js automatically falls back to polling.
### Verify the install
```ts twoslash
import {
createSilentSwapClient,
type SilentSwapClient,
} from '@silentswap/sdk';
import { bsc } from 'viem/chains';
const client: SilentSwapClient = createSilentSwapClient({
baseUrl: 'http://localhost:3001',
chain: bsc,
});
client.getOrder;
```
## Integrator fees
Set integratorFee in USDC base units. A positive fee requires
integratorAddress.
```ts twoslash
import type { QuoteRequest } from '@silentswap/sdk';
const intent: QuoteRequest = {
privacy: true,
inputAddress: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
sourceAsset: 'USDC',
sourceChainId: 56,
outputs: [{
address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
chainId: 8453,
amount: 100_000_000n,
}],
integratorFee: 500_000n,
integratorAddress: '0x90F79bf6EB2c4f870365E785982E1f101E93b906',
};
void intent;
```
This example charges 0.5 USDC. Display quote.integratorFee and
quote.integratorAddress on the review surface. The user funds output
payouts plus service and integrator fees.
V2's pro and integratorId fields are removed.
## Order statuses
The main path is:
AWAITING\_DEPOSIT → OPEN → FULFILLING → FULFILLED → CLAIM\_READY → COMPLETED
| Status | Meaning |
| ------------------------------ | ---------------------------------------------------------------------------------------------- |
| AWAITING\_DEPOSIT | Ticket exists; the quoted deposit has not been observed. |
| OPEN | Deposit is confirmed and fulfillment can begin. |
| FULFILLING | One or more destination payments are in progress. |
| FULFILLED | Destination delivery completed; notary work remains. |
| CLAIM\_READY | The claim is signed and ready to submit. |
| COMPLETED | Claim completed; the order is terminal. |
| DROPPED | The funding window expired; a late deposit can still resurrect the order to OPEN. |
| FAILED | Fulfillment failed; recovery/refund rules apply. |
| ABORTED | Failure recovery finished; the order is terminal. |
```ts twoslash
import type { OrderStatus } from '@silentswap/sdk';
const watcherTerminal = new Set([
'COMPLETED',
'FAILED',
'ABORTED',
]);
function isWatcherTerminal(status: OrderStatus) {
return watcherTerminal.has(status);
}
```
For split orders, render each OrderStateRecipient.status as well as the top-level
state. A top-level label alone can hide a partially delivered split.
## SDK quickstart
Create a viem wallet client for the active account, then bind SilentSwap to the gateway chain.
```ts twoslash
import {
createSilentSwapClient,
} from '@silentswap/sdk';
import {
createWalletClient,
custom,
parseUnits,
type Address,
} from 'viem';
import { mainnet } from 'viem/chains';
declare const provider: Parameters[0];
declare const account: Address;
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: custom(provider),
});
const client = createSilentSwapClient({
walletClient,
});
const quote = await client.quote({
privacy: true,
inputAddress: account,
sourceAsset: 'USDC',
sourceChainId: 1,
outputs: [{
address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
chainId: 8453,
amount: parseUnits('25', 6),
}],
});
// Render every output and fee before this line.
const order = await client.placeOrder(quote);
const stop = client.trackOrderViaWebSocket(
order.reference,
(state) => console.log(state.status),
console.error,
);
stop;
```
Keep the unsubscribe function for component cleanup. For a narrated approval step, call
hasAllowance, then ensureAllowance, before
placeOrder; the latter safely rechecks allowance.
## Quotes
quote accepts an outputs\[] array. Private wallet-funded requests allow
1–20 outputs; address-funded and Simple Bridge requests require exactly one.
### Curated input asset
Use sourceAsset for an asset from @silentswap/assets.
```ts twoslash
import type {
QuoteRequest,
SilentSwapClient,
} from '@silentswap/sdk';
declare const client: SilentSwapClient;
const intent: QuoteRequest = {
privacy: true,
inputAddress: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
sourceAsset: 'ETH',
sourceChainId: 1,
outputs: [{
address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
chainId: 8453,
amount: 100_000_000n,
}],
};
const quote = await client.quote(intent);
```
### Discovered input token
Use sourceToken for a token outside the curated registry. Supply the live
address/mint, decimals, and symbol from a token-data source. Never set both
sourceAsset and sourceToken.
### Arbitrary output asset
Set outputs\[0].dest with the destination asset ID, live decimals, kind, and
slippage. The quote returns dest.minDestAmount; display it as the minimum received.
Arbitrary-output routes are currently single-recipient.
```ts twoslash
import type { QuoteRequest } from '@silentswap/sdk';
const outputIntent: QuoteRequest = {
privacy: true,
inputAddress: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
sourceAsset: 'USDC',
sourceChainId: 56,
outputs: [{
address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
chainId: 1,
amount: 50_000_000n,
dest: {
kind: 'evm',
chainId: 1,
assetId: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
assetSymbol: 'WETH',
assetDecimals: 18,
slippageBps: 100,
},
}],
};
void outputIntent;
```
## Refunds
executeRefund is for private EVM-origin orders whose gateway refund window has opened. It
fetches fresh order state, validates preconditions, dry-runs the contract call, broadcasts,
and waits for the receipt.
```ts twoslash
import {
RefundNotAllowedError,
RefundRevertedError,
type SilentSwapClient,
} from '@silentswap/sdk';
import type { PrivateOrderReference } from '@silentswap/sdk';
declare const client: SilentSwapClient;
declare const reference: PrivateOrderReference;
try {
const { refundTxHash } = await client.executeRefund(reference);
console.log(refundTxHash);
} catch (error) {
if (error instanceof RefundNotAllowedError) {
console.warn(error.message);
} else if (error instanceof RefundRevertedError) {
console.error(error.refundTxHash);
} else {
throw error;
}
}
```
Refunds are rejected for non-EVM origins, non-OPEN/FAILED states,
unexpired orders, a different depositor wallet, already fulfilled/refunded orders, or any split
where a recipient is paid or in flight.
Do not implement a local timer as the authorization source. Let the fresh API state and the
SDK simulation decide whether the wallet may open.
## Split payouts
Put 1–20 entries in outputs. Each amount is the USDC payout in
base units. The user funds the sum of payouts plus service and integrator fees.
```ts twoslash
import type { QuoteRequest } from '@silentswap/sdk';
const split: QuoteRequest = {
privacy: true,
inputAddress: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
sourceAsset: 'USDC',
sourceChainId: 56,
outputs: [
{
address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
chainId: 8453,
amount: 40_000_000n,
},
{
address: '0x90F79bf6EB2c4f870365E785982E1f101E93b906',
chainId: 56,
amount: 60_000_000n,
},
],
};
void split;
```
Render the requested outputs alongside quote.recipients\[] during review and
state.recipients\[] during tracking. Do not rely on the deprecated top-level
recipient, payoutAmount, or recipientTxHash mirrors.
A split cannot currently include an arbitrary-asset dest. Request separate orders
if recipients need different output assets.
## Tracking
trackOrderViaWebSocket accepts a serializable reference and emits the current state
plus later status or transaction-hash changes. The compatibility name is stable; private mode
uses browser SSE with a polling backstop, while Simple Bridge uses status polling.
```ts twoslash
import type {
PrivateOrderReference,
SilentSwapClient,
} from '@silentswap/sdk';
declare const client: SilentSwapClient;
declare const reference: PrivateOrderReference;
const unsubscribe = client.trackOrderViaWebSocket(
reference,
(state) => {
console.log(state.status);
},
(error: unknown) => {
console.error('tracking failed', error);
},
);
unsubscribe;
```
Store and call unsubscribe when the owning screen unmounts. The watcher also
closes on COMPLETED, FAILED, or ABORTED, and after its
two-hour safety limit. It deliberately keeps watching DROPPED orders because a
late source-chain deposit can resurrect them to OPEN.
Use getOrder(reference) for one-shot private recovery after navigation. Do not
start a second custom polling loop beside trackOrderViaWebSocket.
## Deposits
Inspect the quoted deposit shape before opening a wallet.
```ts twoslash
import {
isBtcPsbtDeposit,
isUtxoDeposit,
type PrivateQuote,
} from '@silentswap/sdk';
declare const quote: PrivateQuote;
if (isUtxoDeposit(quote.deposit)) {
console.log(quote.deposit.payinAddress, quote.deposit.expectedAmount);
} else if (isBtcPsbtDeposit(quote.deposit)) {
console.log(quote.deposit.psbt, quote.deposit.expectedSats);
} else {
switch (quote.deposit.txKind ?? 'evm') {
case 'svm':
console.log(quote.deposit.solanaTx);
break;
case 'ton':
console.log(quote.deposit.tonMessages);
break;
case 'tron':
console.log(quote.deposit.tronTransaction);
break;
default:
console.log(quote.deposit.to, quote.deposit.data);
}
}
```
### EVM deposits
After review, placeOrder checks allowance, simulates the deposit,
broadcasts it on deposit.chainId, and waits for the receipt. It rejects approval
calldata, empty calldata, and zero-address destinations before signing.
For native tokens no approval is needed. For ERC-20 inputs, show the approval and deposit as
distinct wallet steps.
### Native-wallet deposits
Supply narrow Solana, TON, TRON, or Bitcoin adapters beside the viem wallet. The SDK verifies
the active address and payload before it invokes an adapter. It never discovers an injected
wallet or chooses between brands.
## Solana deposits
A Solana-source quote has txKind: 'svm' and a base64
solanaTx. The API builds the versioned transaction; the active Solana wallet
reviews and signs it.
```ts twoslash
import type { PrivateQuote } from '@silentswap/sdk';
declare const quote: PrivateQuote;
if (!('type' in quote.deposit) && quote.deposit.txKind === 'svm') {
const encodedTransaction = quote.deposit.solanaTx;
if (!encodedTransaction) throw new Error('Missing quoted Solana transaction');
// placeOrder validates the payer, then invokes the configured Solana adapter.
console.log(encodedTransaction, quote.ticket);
}
```
The SDK requires the adapter address to match inputAddress. Phantom must use
Phantom's provider; MetaMask multichain must use its own session. Never fall back to a generic
window\.solana provider, because that can expose another wallet brand's account.
The SDK accepts the serialized solanaTx and rejects a mismatched payer before the
wallet opens.
## TON and TRON deposits
Account-chain quotes are signed outside viem.
```ts twoslash
import type { PrivateQuote } from '@silentswap/sdk';
declare const quote: PrivateQuote;
if (!('type' in quote.deposit)) {
if (quote.deposit.txKind === 'ton') {
const messages = quote.deposit.tonMessages;
if (!messages?.length) throw new Error('Missing TON messages');
console.log(messages);
}
if (quote.deposit.txKind === 'tron') {
const transaction = quote.deposit.tronTransaction;
if (!transaction) throw new Error('Missing TRON transaction');
console.log(transaction);
}
}
```
For wallet-backed routes, configure the corresponding narrow adapter. The SDK verifies its
active address against inputAddress, then submits all quoted TON messages in order
or the single quoted TRON transaction without changing calldata or value.
Some TON/TRON inputs use an external pay-in-address shape instead. Detect that with
isUtxoDeposit and show the returned address and amount.
## Bitcoin and Litecoin deposits
SilentSwap supports two UTXO deposit shapes.
### Pay-in address
A utxo deposit is an external transfer. Show the exact address, currency, and
amount. The funds may come from a wallet or exchange.
```ts twoslash
import {
isUtxoDeposit,
type PrivateQuote,
type SilentSwapClient,
} from '@silentswap/sdk';
declare const client: SilentSwapClient;
declare const quote: PrivateQuote;
if (isUtxoDeposit(quote.deposit)) {
const payment = {
address: quote.deposit.payinAddress,
amount: quote.deposit.expectedAmount,
currency: quote.deposit.currency,
};
const order = await client.placeOrder(quote);
console.log(payment, order.reference);
}
```
Without a matching wallet capability, placeOrder returns
awaiting-external-deposit with the same pay-in instructions.
### Bitcoin PSBT
A btc-psbt deposit spends the quoted refundTo account's UTXOs. The SDK
audits every output, the refund address, inputs, absolute fee, and fee rate before invoking the
Bitcoin adapter. The adapter receives the decoded audit so it can display the exact spend.
If signing or broadcast fails, keep the ticket and the rejection capability so the UI can
recover or explicitly abandon the still-unfunded order.