# 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
pnpm add @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.
Browser integrators can test on the default port-3000 localhost origins and must register their
exact HTTPS production origins. Complete [testing and production access](/testing) before launch.
## 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.1.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.
## Testing and production access
Every browser integration must be registered with SilentSwap. Registration binds an issued
integratorId to exact browser origins and to the fee configuration controlled by
SilentSwap. Integrators cannot choose their own fee percentage, fee split, or payout address in
SDK calls.
### Configure the client
SilentSwap supplies the integrator ID after approving the integration and its origins. Configure
it once on the client, not on individual quotes:
```ts twoslash
import { createSilentSwapClient } from '@silentswap/sdk';
const client = createSilentSwapClient({
integratorId: 'int_0123456789abcdef01234567',
});
void client;
```
An unknown, inactive, or origin-mismatched ID is rejected by the API.
### Test from localhost
Every active integrator is allowed from these development origins by default:
```text
http://localhost:3000
http://127.0.0.1:3000
```
Run the browser integration on port 3000 and configure the issued
integratorId. Local origins are shared, so the ID is required to select the correct
integrator configuration. Other ports are not allowlisted.
These localhost defaults grant browser access to the production API. They are not a sandbox:
* requesting and rendering quotes does not move funds;
* placeOrder() can request approvals and submit real-network transactions;
* use a deliberately small amount and explicit real-funds safeguards for execution tests.
SilentSwap does not currently provide a no-money testnet environment.
### Get whitelisted for production
Before launch, send SilentSwap support every exact HTTPS production origin. Include each scheme,
subdomain, and non-default port separately:
```text
https://swap.example.com
https://www.example.com
```
Do not send page URLs such as [https://swap.example.com/swap](https://swap.example.com/swap); origins never contain a
path. Preview-deployment domains must be registered individually because wildcard domains are not
allowed.
Production browser calls will fail until the origin is whitelisted and attached to the active
integrator ID. Node and server-to-server clients do not encounter browser CORS, but they still need
a registered integrator ID for SilentSwap-managed fees.
### Verify browser access
After SilentSwap confirms the origin, verify the preflight before testing the full flow:
```sh
curl -i -X OPTIONS https://api.silentswap.com/health \
-H 'Origin: https://swap.example.com' \
-H 'Access-Control-Request-Method: GET'
```
The response should include:
```text
Access-Control-Allow-Origin: https://swap.example.com
```
Continue with the [quickstart](/sdk/quickstart), then test quote review, wallet rejection,
tracking teardown, and refund error handling before enabling real execution for users.
## Migrate from SilentSwap V2
@silentswap/sdk\@2.1.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 }) |
| 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 / old integratorId | new owner-issued integratorId in client config |
| React/Vue/widget packages | discontinued; React 2.x is planned |
Do not reuse the old pro or integrator ID. SilentSwap enables the standard port-3000
localhost origins, registers the application's production origins, issues a new ID, and controls
the total fee, integrator/SilentSwap split, and payout address from the admin dashboard. Configure that ID once on
createSilentSwapClient; quote requests do not accept fee fields.
#### 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,
// Add only after SilentSwap approves the integration and issues an ID.
// integratorId: 'int_0123456789abcdef01234567',
});
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.
* Registered integrators configure an owner-issued integratorId once on the client.
Total fee, integrator/SilentSwap split, and payout address are server-controlled; the resulting
service and integrator fee amounts are returned on the quote for review.
* Browser integrations must register their exact HTTPS production origin with SilentSwap
support. Local browser testing defaults to [http://localhost:3000](http://localhost:3000) and
[http://127.0.0.1:3000](http://127.0.0.1:3000). 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. |
| integratorId | Registered integrators | Public ID issued by SilentSwap; fee and origins are owner-controlled. |
### 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,
DepositGasPolicyError,
DepositRevertedError,
DepositSimulationError,
ExactInputMismatchError,
HttpError,
MaintenanceModeError,
PublicSwapRevertedError,
PublicSwapSimulationError,
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 ExactInputMismatchError) {
return `Quote debit mismatch: asked ${error.requestedAmount}, got ${error.returnedAmount ?? 'nothing'}`;
}
if (error instanceof DepositGasPolicyError) {
return error.unsupportedReason === 'gas-budget'
? `Gas cost ${error.maxGasCostWei} exceeds your budget ${error.gasBudgetWei}`
: `Route needs ${error.gas} gas — above the supported cap`;
}
if (error instanceof DepositSimulationError) return error.message;
if (error instanceof DepositRevertedError) return `Reverted: ${error.depositTxHash}`;
if (error instanceof PublicSwapSimulationError) return error.message;
if (error instanceof PublicSwapRevertedError) return error.message;
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.
### Nothing was broadcast
DepositGasPolicyError is thrown before any wallet request when the route falls
outside deposit gas policy. Read unsupportedReason to tell the two cases apart:
'execution-cap' means a clean estimate exceeded the maximum supported execution
probe, and 'gas-budget' means the cost exceeded the gasBudgetWei
you passed. Both carry gas, and the budget case also carries
maxGasCostWei and gasBudgetWei. Neither leaves a pending
transaction — tell the user to pick a different route rather than to wait.
ExactInputMismatchError is thrown when an exact-input quote does not report the
debit you asked for. requestedAmount and returnedAmount are both
base-unit strings; returnedAmount is undefined when the quote
reported no debit at all.
### Distinguishing reverts
isRevertError(err) walks an unknown error's cause chain and reports
whether it bottoms out in a contract revert, rather than matching node-specific message
strings. Use it to separate a genuine on-chain rejection from an RPC or transport failure —
the first is worth surfacing to the user, the second is worth retrying.
```ts twoslash
import { isRevertError } from '@silentswap/sdk';
export function isWorthRetrying(error: unknown): boolean {
return !isRevertError(error);
}
```
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
pnpm add @silentswap/sdk viem
```
The public package graph is @silentswap/sdk → @silentswap/assets →
@silentswap/chains. All three packages ship at version 2.1.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
Integrator fees are controlled by SilentSwap through the admin dashboard. Each integrator has a
total percentage and a configurable split between the integrator and SilentSwap. Integrators
receive a public ID; they cannot pass a fee percentage, split, or payout address on individual
quote requests.
```ts twoslash
import { createSilentSwapClient } from '@silentswap/sdk';
const client = createSilentSwapClient({
integratorId: 'int_0123456789abcdef01234567',
});
const quote = await client.quote({
privacy: true,
inputAddress: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
sourceAsset: 'USDC',
sourceChainId: 56,
outputs: [{
address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC',
chainId: 8453,
amount: 100_000_000n,
}],
});
void quote;
```
The API calculates an additive percentage from the requested private payout. With the default 1%
total fee and 30/70 split, a 100-USDC output remains 100 USDC to the recipient, 0.30 USDC goes to
the integrator, 0.70 USDC goes to SilentSwap, and the user funds 101 USDC. The two shares always
sum to the configured total.
An integrator can be registered without a fee payout address. The user pays the same additive
total, but with no address to receive the integrator share, SilentSwap keeps the whole fee and
`quote.integratorFee` is `0`. Adding an address later activates the configured split.
Both shares are settled on-chain as entries in the deposit itself: the gateway pulls
`payout + fees` and pays each fee wallet directly when the order is claimed. An order carries
either an integrator fee or SilentSwap's own platform fee, never both — a registered integrator's
users are charged the integrator's configured percentage and nothing else.
Display quote.serviceFee, quote.integratorFee, and
quote.integratorAddress on the trusted review surface. The service fee can also include
any separate route-level SilentSwap fee.
The fee is currently applied only where the private route supports on-chain fee settlement.
Address-funded BTC/LTC/TON/TRON inputs and Simple Bridge do not add an integrator fee.
See [testing and production access](/testing) for the default local origins and the required
production-origin whitelist before launch.
## 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);
```
### Exact input
Set amount on a wallet-funded private request to spend exactly what the user
typed, in **source-token** base units. The wallet pulls precisely that amount, and
outputs\[].amount stop being targets — they become ratio weights, with the API
sizing real payouts from the route's guaranteed minimum output.
```ts twoslash
import type { QuoteRequest, SilentSwapClient } from '@silentswap/sdk';
declare const client: SilentSwapClient;
const exactInput: QuoteRequest = {
privacy: true,
inputAddress: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
sourceAsset: 'ETH',
sourceChainId: 1,
amount: 1_000_000_000_000_000_000n, // spend exactly 1 ETH
outputs: [
{ address: '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC', chainId: 8453, amount: 70n },
{ address: '0x90F79bf6EB2c4f870365E785982E1f101E93b906', chainId: 8453, amount: 30n },
],
};
const quote = await client.quote(exactInput);
```
The weights above split 70/30. Omit amount to keep the legacy behavior where
outputs\[].amount are exact USDC targets.
Exact input is not valid with utxoSource or cexSource — those rails
are already exact-input — nor with integrator orders. BTC/LTC requests
(UtxoFundedPrivateQuoteRequest) still require amount and a single
output; TON/TRON (AccountFundedPrivateQuoteRequest) accept weighted splits
through a wallet adapter, though the adapter-less ChangeNOW fallback stays single-output at
runtime.
#### Payout figures are estimates until the deposit lands
On an exact-input order the quote's payoutAmount is a pre-deposit estimate. The
gateway deposit event's credited amount becomes the authoritative principal and resizes every
recipient pro-rata. Render these with "≈" — see [Tracking](/sdk/tracking) for the
payoutsAreEstimates flag that tells you when the number has firmed up.
### 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.
### Estimated payouts
While state.payoutsAreEstimates is true, the order is exact-input and still
awaiting its deposit: every payout figure is a preview that the gateway event's credited
amount will resize pro-rata. Render amounts with "≈" until the flag clears, so a number that
is about to move is never shown as final.
```ts twoslash
import type { OrderState } from '@silentswap/sdk';
export function formatPayout(state: OrderState, amount: string): string {
return state.payoutsAreEstimates ? `≈ ${amount}` : amount;
}
```
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.
### Deposit gas
placeOrder, previewDepositCost, and
hasSufficientGasForDeposit take an optional DepositGasOptions.
Setting gasBudgetWei caps the native spend on gas and enables bounded escalation
when eth\_estimateGas reverts; omit it to keep wallet-selected fees.
Gas resolution is shared between the preflight and the broadcast, so the number the user is
checked against is the number that gets sent — **pass the same options to both or the check
means nothing**.
```ts twoslash
import { type PrivateQuote, type SilentSwapClient } from '@silentswap/sdk';
declare const client: SilentSwapClient;
declare const quote: PrivateQuote;
const opts = { gasBudgetWei: 5_000_000_000_000_000n }; // 0.005 native
const cost = await client.previewDepositCost(quote, opts);
if (!cost.supported) {
// 'execution-cap' — route needs more gas than the protocol supports.
// 'gas-budget' — route is fine, your budget is too low.
throw new Error(`Route unavailable: ${cost.unsupportedReason}`);
}
const gas = await client.hasSufficientGasForDeposit(quote, opts);
if (!gas.ok) throw new Error(`Need ${gas.needWei} wei, have ${gas.haveWei}`);
await client.placeOrder(quote, opts);
```
Check supported before ok. An unsupported route fails the policy
regardless of balance, so topping the wallet up will not help — telling the user to add funds
would send them down a dead end. UTXO and BTC-PSBT deposits are off-EVM and always report
supported: true with zero cost.
### 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.