# 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.