# Mesh Connect — llms-full.txt

Complete condensed reference for AI coding assistants. Covers all Mesh Guides across all five SDK platforms (Web, iOS, Android, React Native, Flutter). Following the llms.txt standard (https://llmstxt.org).

**Sandbox base URL**: `https://sandbox-integration-api.meshconnect.com`
**Production base URL**: `https://integration-api.meshconnect.com`
**Auth headers**: `X-Client-Id: <clientId>` and `X-Client-Secret: <apiKey>` on every server-side request.

---

## Prepare to build

Get a Mesh dashboard account at [dashboard.meshconnect.com](https://dashboard.meshconnect.com). Create a sandbox API key under Account > API keys. Add allowed domains under Account > API keys > Access (include localhost:3000 for local dev).

**SDK installs**: Web: `npm install @meshconnect/web-link-sdk` | iOS: Swift Package Manager or CocoaPods -> `https://github.com/FrontFin/mesh-ios-sdk` | Android: `implementation 'com.meshconnect:link:<version>'` in `build.gradle` | React Native: `npm install @meshconnect/react-native-link-sdk react-native-webview` | Flutter: `mesh_sdk_flutter` in `pubspec.yaml`

---

## 15-Minute Quickstart

5-step end-to-end integration using the Web SDK: (1) `npm install @meshconnect/web-link-sdk` (2) `POST /api/v1/linktoken` from your server (3) pass `linkToken` to client (4) call `createLink({...callbacks})` then `connection.openLink(linkToken)` (5) handle callbacks.

**Sandbox test credentials** (all accounts use same password/OTP): Password `Pass123`, OTP `123456`. Username `Mesh` = full portfolio for general testing.

**Key networkIds**: Solana `0291810a-5947-424d-9a59-e88bb33e999d` | Ethereum `e3c7fdd8-b1fc-4e51-85ae-bb276e075611` | Base `aa883b03-120d-477c-a588-37c2afd3ca71`

**Minimal Link Token request (deposit)**: `POST https://sandbox-integration-api.meshconnect.com/api/v1/linktoken` with headers `X-Client-Id` and `X-Client-Secret`, body `{ "userId": "user-123", "transferOptions": { "transferType": "deposit", "toAddresses": [{ "networkId": "0291810a...", "symbol": "USDC", "address": "YOUR_ADDRESS" }] } }`

**Minimal Web SDK init**: `import { createLink } from '@meshconnect/web-link-sdk'` then `const connection = createLink({ onIntegrationConnected, onTransferFinished, onExit })` then `connection.openLink(linkToken)`

---

## Fetch a Link Token

**Endpoint**: `POST /api/v1/linktoken`. Tokens are 10 minutes, single-use. Fetch on button click, not on page load. Request a new one each session.

**Required field**: `userId` — unique, non-PII string, max 300 chars.

**`transferOptions` parameters**: `transferType` (`"deposit"` | `"payment"` | `"onramp"`, omit for withdrawal/verify) | `toAddresses[]` array of `{ networkId, symbol, address }` — all three required for deposit/payment | `amount` required for `payment` type | `isInclusiveFeeEnabled` boolean — fees deducted from `amount` if true | `generatePayLink` boolean | `amountInFiat` fiat display value | `addressTag` memo/tag for XRP, XLM, etc. | `minAmount` / `minAmountInFiat` minimum transfer size | `transactionId` your internal transaction ID

**`verifyWalletOptions`**: `verificationMethods: ["signedMessage"]` | `message` the message to sign | `networkId` OR `networkType` (not both) | `addresses[]` list to verify

**Other top-level fields**: `restrictMultipleAccounts` boolean | `subClientId` for B2B/PSP multi-merchant setups | `integrationId` to pre-select a provider

**Canonical deposit request**: `POST /api/v1/linktoken` body `{ "userId": "UNIQUE_USER_ID", "restrictMultipleAccounts": true, "transferOptions": { "transferType": "deposit", "toAddresses": [{ "networkId": "<solana-id>", "symbol": "USDC", "address": "YOUR_SOL_ADDRESS" }, { "networkId": "<eth-id>", "symbol": "USDC", "address": "YOUR_ETH_ADDRESS" }, { "networkId": "<base-id>", "symbol": "USDC", "address": "YOUR_BASE_ADDRESS" }] } }`

---

## Launch the Mesh SDK

**Web SDK** — `createLink({ renderType, theme, language, displayFiatCurrency, accessTokens, onIntegrationConnected, onTransferFinished, onExit, onEvent })`. Params: `renderType` `'overlay'` or `'embedded'` | `theme` `'system'`/`'dark'`/`'light'` | `language` `'system'` or BCP 47 code | `displayFiatCurrency` ISO 4217, default `'USD'` | `accessTokens` MMT return-user array. Launch: `connection.openLink(linkToken)` (overlay) or `connection.openLink(linkToken, 'custom-iframe-id')` (embedded). Close: `connection.closeLink()`.

**Rendering options**: Overlay = modal popup. Embedded = iframe in your container (min 450px, recommended 665px; pass `renderType: 'embedded'` and a custom iframe ID). Paylink = set `generatePayLink: true` in Link Token -> response includes `paymentLink` URL, no SDK install needed, short-lived single-use.

**iOS (Swift)**: Create `LinkSettings(accessTokens:, language: "system", displayFiatCurrency: "USD", theme: .system)`. Create `LinkConfiguration(linkToken:, settings:, onIntegrationConnected:, onTransferFinished:, onEvent:, onExit:)`. Call `configuration.createHandler()` -> on `.success(let handler)`: set `linkHandler = handler` then `handler.present(in: viewController)`. In `onExit`: call `linkHandler?.showExitAlert()` if `showAlert == true`, else `linkHandler?.closeLink()`.

**Android (Kotlin)**: Register `registerForActivityResult(LaunchLink()) { result -> when(result) { is LinkSuccess -> handle result.payloads; is LinkExit -> handle result.errorMessage } }`. Create `LinkConfiguration(token = linkToken, theme = LinkTheme.SYSTEM, language = "system", displayFiatCurrency = "USD", accessTokens = accessTokens)`. Launch: `linkLauncher.launch(configuration)`.

**React Native**: `<LinkConnect linkToken={linkToken} settings={{ language: 'system', displayFiatCurrency: 'USD', accessTokens }} onIntegrationConnected={...} onTransferFinished={...} onEvent={...} onExit={...} />`

**Flutter**: `await MeshSdk.show(context, configuration: MeshConfiguration(linkToken:, language: 'system', displayFiatCurrency: 'USD', theme: ThemeMode.system, integrationAccessTokens: accessTokens, onEvent:, onError:, onSuccess:, onIntegrationConnected:, onTransferFinished:))`

---

## Use Mesh's Callback Functions

**`onIntegrationConnected`** — fires when user connects an exchange or wallet. Store `payload.accessToken.accountTokens[0].tokenId` and `brokerType` server-side for MMT return-user flows. Full payload: `{ accessToken: { accountTokens: [{ tokenId, brokerType, ... }] }, brokerType, brokerName, ... }`

**`onTransferFinished`** — fires when transfer request acknowledged by provider. Important: NOT on-chain confirmation — use webhooks for business logic. Use only for UI updates (show success screen). Payload: `{ status, transferId, txHash, symbol, amount, amountInFiat, totalAmountInFiat, fromAddress, toAddress, refundAddress, networkId, networkName }`

**`onExit`** — fires whenever Link closes. Check `page` field: `'transferExecutedPage'` = flow completed. Call `connection.closeLink()` here if using embedded mode.

**`onEvent`** — fires for granular user interactions. Useful for analytics. Payload: `{ type: '<eventName>', payload: { ... } }`

**Web**: In `onIntegrationConnected`: extract `payload.accessToken.accountTokens[0]` -> `{ tokenId, brokerType }` -> save to server. In `onTransferFinished`: update UI from `payload.status`. In `onExit`: call `connection.closeLink()`. In `onEvent`: `analytics.track(event.type, event.payload)`.

**iOS (Swift)**: Pass closures to `LinkConfiguration`. In `onIntegrationConnected`: pattern-match `if case .accessToken(let p) = linkPayload` -> extract `p.accountTokens.first?.tokenId`.

**Android (Kotlin)**: Results come via registered `LaunchLink()` activity result: `is LinkSuccess -> result.payloads` contains `IntegrationConnected` / `TransferFinished` payloads. `is LinkExit -> result.errorMessage`.

---

## Supercharge Return Users (MMT — Mesh Managed Tokens)

Skip re-authentication for returning users by passing a stored `tokenId` back to the SDK.

**Step 1 — Capture on first connection**: In `onIntegrationConnected`, extract `const { tokenId, brokerType } = payload.accessToken.accountTokens[0]` and store server-side keyed to userId.

**Step 2 — Build accessTokens array**: `[{ accessToken: '<tokenId>', brokerType: '<brokerType>', brokerName: '', accountId: '', accountName: '' }]` — note: put the tokenId in the `accessToken` field.

**Step 3 — Pass to SDK on return visits**: Web: `createLink({ accessTokens: accessTokens, ...callbacks })` | iOS: `LinkSettings(accessTokens: accessTokens, language: "system", displayFiatCurrency: "USD", theme: .system)` | Android: `LinkConfiguration(token = linkToken, accessTokens = accessTokens, theme = LinkTheme.SYSTEM, language = "system", displayFiatCurrency = "USD")` | React Native: `<LinkConnect settings={{ accessTokens, language: 'system', displayFiatCurrency: 'USD' }} .../>` | Flutter: `MeshConfiguration(integrationAccessTokens: accessTokens, language: 'system', displayFiatCurrency: 'USD', theme: ThemeMode.system, linkToken: linkToken, ...)`

**Token lifecycle**: Same user + same integration -> always same `tokenId`. Revoked tokens permanently disabled (new connection required). Binance: tokens expire but `tokenId` never changes — no DB update needed. Supported integrations: Coinbase, Binance, Uphold. Not available in sandbox.

---

## Polish the Experience

**Entry point UX**: Label button "Direct deposit" or "Pay with crypto". Lead with exchange logos/icons. Offer a dedicated return-user button ("Pay with Coinbase in 1 tap").

**Session lifecycle**: Preload Link session on user login (before they click). Refresh Link Token every 50 minutes (sessions expire at 60 min). Refresh triggers: address change, theme change, connectivity restore, modal close.

**Closing Link**: `closeLinkRequested()` = user-initiated dismiss, shows Mesh confirmation dialog if mid-flow, then fires `onExit` — call this when user clicks your X button. `closeLink()` = programmatic force-close, no confirmation, no `onExit` — use inside `onExit` handler to tear down iframe. Do NOT call `closeLink()` inside `onTransferFinished` — user is still on the success screen.

**Embedded iframe**: Keep in DOM between sessions (hide/show with CSS — don't destroy/recreate). Min width 450px, recommended 665px. In modals: `min(665px, calc(100dvh - Xpx))` where X = modal chrome height (~100–160px).

**iOS plist — `LSApplicationQueriesSchemes` (wallet deep links)**: bitcoin, bitkeep, bitcoincom, bnc, cbwallet, dfw, exodus, ledgerlive, metamask, okx, phantom, rabby, rainbow, robinhood-wallet, tpoutside, tronlinkoutside, trust, uniswap, zengo

**Link customization**: Dashboard > Account > Link Configuration > Interface (fonts, colors, loading animations, logo).

---

## Transfer Status Webhooks

**Setup**: Register callback URI at Dashboard > Account > API keys > Webhooks. Secret shown once — store securely. Separate sandbox and production URIs. Whitelist Mesh IP: `20.22.113.37`

**Signature verification**: Header `X-Mesh-Signature-256`. Compute `crypto.createHmac('sha256', WEBHOOK_SECRET).update(rawBody).digest('base64')` over **raw request body bytes** (not parsed JSON) and compare to the header value. Critical: never parse/re-serialize the body before computing the HMAC. Use `express.raw({ type: '*/*' })` NOT `express.json()` on the webhook route. Respond with 200 in under 200ms, then process asynchronously.

**Webhook handler pattern**: Route with `express.raw({ type: '*/*' })` -> read `req.body` as raw Buffer -> compute HMAC-SHA256 Base64 digest -> compare to `req.headers['x-mesh-signature-256']` -> if mismatch return 400 -> else `res.sendStatus(200)` -> then `processAsync(JSON.parse(rawBody))`

**Delivery semantics**: At-least-once delivery — deduplicate on `EventId` (stable across retries; `Id` changes per delivery). `Pending` webhook not guaranteed in sandbox.

**Transfer payload fields**: `EventId` (idempotency key) | `TransferId` | `TransactionId` (your ID from Link Token) | `TransferStatus`: `Pending` | `Succeeded` | `Failed` | `RefundPending` | `RefundSucceeded` | `TxHash` | `Token` | `Chain` | `SourceAmount` | `DestinationAmount` | `DestinationAddress` | `SourceAddress` | `RefundAddress` | `RefundType` | `TransferSteps[]` for bridge/multi-step transfers (each has `StepOrder`, `Chain`, `Token`, `TxHash`, `SourceAddress`)

**Flow**: `Pending` -> `Succeeded` | `Failed`. Use **`Succeeded`** to credit users / release inventory, never `onTransferFinished`.

---

## Prepare for Go-Live

1. **Production webhook URI**: Dashboard > Account > API keys > Webhooks > Production (separate from sandbox — sandbox webhook URIs don't carry over).
2. **Update to latest SDK version**: Check GitHub releases before go-live (versions + release notes live here). Web: `https://github.com/FrontFin/mesh-web-sdk/releases` | iOS: `https://github.com/FrontFin/mesh-ios-sdk/releases` | Android: `https://github.com/FrontFin/mesh-android-sdk/releases` | React Native: `https://github.com/FrontFin/mesh-react-native-sdk/releases` | Flutter: `https://github.com/FrontFin/mesh-flutter-sdk/releases`. Release notes and current versions: /resources/sdk-release-notes. Stay updated automatically: GitHub Watch > Custom > Releases (email per release), or Dependabot (`.github/dependabot.yml`; ecosystems: npm, gradle, pub, swift; scope with `allow: dependency-name` to Mesh packages, e.g. `@meshconnect/*`) to auto-open PRs on new versions.
3. **Production API key**: Dashboard > Account > API keys > API keys > Production. Permissions: Read-only or Read & Write (required for transfers). Prerequisites: 2FA enabled + Mesh business verification complete.
4. **Production base URL**: `https://integration-api.meshconnect.com` (Sandbox: `https://sandbox-integration-api.meshconnect.com`)
5. **Content Security Policy**: `frame-src`: add `*.meshconnect.com`. Tron only — `connect-src`: add `https://tron.twnodes.com`, `https://trx.mytokenpocket.vip`, `https://api.trongrid.io`

---

## Add Mesh Onramp Integrations

Let users buy crypto with fiat and send it directly to your address.

**Link Token for onramp**: Set `transferOptions.transferType: "onramp"`, `isInclusiveFeeEnabled: true`, `amountInFiat: <amount>`, and `toAddresses: [{ networkId, symbol, address }]`.

**Optional: pre-select a provider** by passing `integrationId` in Link Token body (get integrationId from `GET /api/v1/transfers/managed/integrations`).

**Optional: Quote API** — `POST /api/v1/transfers/managed/quote` with body `{ amountInFiat, fiatCurrency, symbol, networkId, toAddress, brokerType }`. Response includes `minAmountFiat` (show to users before amount entry), `isEligible` (check before displaying a provider), and fee breakdown.

**Exchange icons**: Use `logoUrl` from the integrations endpoint.

---

## Add Mesh to Withdrawal Flow

Enable users to withdraw from your platform back to their exchange/wallet.

**Webhook `refundAddress`**: In your `transfer.succeeded` webhook, save `RefundAddress` + `Chain` + `Token` + `SourceAccountProvider` per userId. This is the address to send funds back to.

**Retrieve user deposit address** — `POST /api/v1/transfers/managed/address/list` with body `{ type: "<brokerType or 'deFiWallet'>", authToken: "<tokenId>", symbol: "USDC", networks: [{ networkId: "..." }] }`. Response includes address per network, `networkName`, `logoUrl`, `memo` (critical for XRP, XLM — always display to user).

**Withdrawal Link Token**: Minimal body is just `{ "userId": "UNIQUE_USER_ID", "restrictMultipleAccounts": true }` (no `transferOptions` needed). Add `integrationId` to pre-select provider. Set `restrictMultipleAccounts: false` for address book / multi-account use cases.

---

## Verify Self-Hosted Wallets

Prove a user owns a specific wallet address via message signing.

**Link Token for wallet verification**: Include `verifyWalletOptions: { verificationMethods: ["signedMessage"], message: "Sign this message...<nonce>", networkId: "<networkId>" }`. Use `networkId` OR `networkType`, never both. Can be combined with a deposit/payment Link Token.

**`walletMessageSigned` event payload** (via `onEvent`): `signedMessageHash` (store immediately — Mesh does not retain it) | `message` | `address` | `timeStamp` | `isVerified`

**`verifyWalletRejected`**: fires if user declines to sign.

**Testing**: MetaMask on Sepolia or Phantom on Solana Devnet.

---

## How It All Fits Together

Architecture: 3-party model — your server <-> Mesh API, your client <-> Mesh SDK (Link), Mesh -> your webhook endpoint.

**4-step integration flow**: (1) Server -> Mesh: `POST /api/v1/linktoken` -> returns `linkToken`, configures full user session. (2) Client -> SDK: pass `linkToken` to `createLink()` / `LinkConfiguration` / `MeshConfiguration`, call `openLink()`, Mesh handles all UX. (3) SDK -> Client callbacks: `onIntegrationConnected` save `tokenId` for MMT | `onTransferFinished` update UI | `onExit` check `page` field | `onEvent` analytics. (4) Mesh -> Server webhooks: `Pending` -> `Succeeded` or `Failed`, use `Succeeded` to credit users, always verify HMAC-SHA256 signature.

**Critical**: `onTransferFinished` fires when the provider acknowledges the request, not on-chain confirmation. Always use the `transfer.succeeded` webhook for final confirmation.

**Return users (MMT)**: Store `tokenId` from `onIntegrationConnected`. Pass as `accessTokens` on next session -> user skips re-auth entirely.

**Platforms**: Web, iOS, Android, React Native, Flutter — all use the same Link Token; initialization pattern differs per SDK.

---

## Concepts Glossary

**Link**: Mesh's hosted UX (iframe / native view) for connecting accounts, initiating transfers, verifying wallets. Available on 5 SDKs: Web, iOS, Android, React Native, Flutter.

**Link Token**: Short-lived (10 min), single-use token requested server-side via `POST /api/v1/linktoken`. Configures the entire user session. Every session starts with one.

**Integrations**: Exchanges (Coinbase, Binance, etc.) and wallets (MetaMask, Phantom, etc.) that Mesh connects to. Mesh handles all auth flows.

**Mesh Managed Tokens (MMT)**: Mesh stores and refreshes OAuth tokens on your behalf. You store a stable `tokenId`. User skips re-auth on return visits. `tokenId` never changes even if underlying access token rotates.

**SmartFunding**: Auto-selects best source asset for a transfer based on available balance, network compatibility, fees, and speed.

**refundAddress**: Address funds can be returned to. For exchange transfers, this is a user deposit address within the exchange (not the originating on-chain address, which is an omnibus hot wallet). Returned in webhooks and `GET /api/v1/transfers/managed/mesh`.

**Sub-clients**: Child Mesh accounts for B2B/PSP use cases. Each gets independent branding, API credentials, and compliance settings.

**Webhooks**: Server-to-server event notifications from Mesh. Use `EventId` as idempotency key (stable across retries; `Id` changes per delivery). At-least-once delivery. Register at Dashboard > Account > API keys > Webhooks.

**Transfer types**: `deposit` user sends to your address | `payment` deposit with fixed `amount` | `onramp` user buys with fiat | `withdrawal` retrieve user deposit addresses (no `transferType` in Link Token) | `verify` wallet ownership via `verifyWalletOptions` | types can be combined

---

## Multi-Language Support

Configure the `language` parameter to display Link in the user's preferred language. Set in SDK initialization on all 5 platforms. Use `'system'` to auto-detect device/browser locale, or a BCP 47 code (e.g. `'en-US'`, `'fr'`, `'zh-CN'`).

**Fallback chain**: unknown region -> language default (e.g. `fr-FR` for any French variant) -> `en-US`. Unsupported language -> `en-US`.

**Currently live**: en-US, zh-CN, fi-FI, fr-FR, de-DE, hi-IN, id-ID, ja-JP, ms-MY, pl-PL, pt-PT, ru-RU, es-US, th-TH, tr-TR, uk-UA, uz-UZ, vi-VN

**Backlog (not yet live)**: ar-EG, zh-HK, zh-TW, cs-CZ, da-DK, nl-NL, en-AU, en-GB, en-IN, fr-CA, el-GR, he-IL, hu-HU, it-IT, ko-KR, no-NO, pt-BR, sk-SK, es-ES, sv-SE

---

## Fiat Currency Support

Configure the `displayFiatCurrency` parameter to show fiat amounts in the user's preferred currency throughout Link. Set in SDK initialization on all 5 platforms. Use ISO 4217 currency codes. Defaults to `'USD'` if not specified.

**Currently live**: USD (US Dollar) | EUR (Euro) | GBP (British Pound)

**Backlog (not yet live)**: CNY, JPY, CHF, CAD, AUD, KRW, HKD, INR, BRL, MXN, SGD, TRY, SAR, AED, ZAR, THB

Affects all fiat-equivalent display values throughout Link. Does not affect which currencies users can transact in — only the display conversion. `displayFiatCurrency` and `language` are set in the same SDK initialization call.

---

## Mesh SDK Events

Complete reference of all SDK events fired via the `onEvent` callback during a Link session.

**Core events**: `pageLoaded` (Link launched, no payload) | `integrationConnected` (user connected account; payload: `integrationType`, `integrationName`, `accessToken` containing `tokenId`) | `transferPreviewed` (user viewed preview; payload: `symbol`, `amount`, `amountInFiat`, `toAddress`, `fees` with `institutionTransferFee`/`estimatedNetworkGasFee`/`customClientFee`, `previewId`) | `transferInitiated` (user clicked Proceed; same payload as `transferPreviewed`) | `transferCompleted` (user reached Success page, triggers `onTransferFinished`; use `onTransferFinished` for business logic, this event for analytics only) | `transferNoEligibleAssets` (no matching assets; payload: `arrayOfTokensHeld` each with `symbol`, `amount`, `amountInFiat`, `ineligibilityReason`) | `close` (user dismissed Link; payload: `page`; if `page === 'transferExecutedPage'` -> full flow completed)

**Wallet verification events**: `walletMessageSigned` (payload: `signedMessageHash`, `message`, `address`, `timeStamp`, `isVerified`) | `verifyWalletRejected` (no payload) | `verifyDonePage` (no payload)

**Other notable events**: `integrationSelected` | `transferDeclined` | `connectionDeclined` | `transferExecutionError` | `linkTransferQRGenerated` | `executeFundingStep` | `done` (read-only or verify flow complete; `page` = `verifyDonePage` or `integrationConnectedPage`) | `methodSelected` | `transferAssetSelected` | `transferNetworkSelected` | `transferAmountEntered` | `fundingOptionsViewed` | `fundingOptionsUpdated` | `gasIncreaseWarning`

---

## Sandbox & Testnets

Full production replica for testing. Uses simulated exchange accounts and real blockchain testnets. No real assets move.

**Sandbox base URL**: `https://sandbox-integration-api.meshconnect.com`

**CEX test accounts** (all use password `Pass123`, MFA/OTP `123456`):
- `Mesh`: full portfolio (BTC, ETH, BNB, USDC, USDT, SOL, XRP+), $10M — general testing
- `Mesh2`: empty portfolio, $0 — empty state / zero balance testing
- `Mesh3`: no crypto, $10M cash only — buy/onramp testing
- `Mesh4`: full portfolio, $100M — large transaction testing
- `MeshBTC` / `MeshUSDC` / `MeshUSDT` / `MeshETH` / `MeshSOL`: single-asset portfolios for asset-specific flows

**Wallet testnets**: MetaMask: Sepolia (`SEPOLIAETH`, stablecoins: PYUSD, USDG) and Base Sepolia (`SEPOLIAETH`, stablecoins: USDC, EURC). Rainbow: Sepolia (`SEPOLIAETH`, stablecoins: PYUSD, USDG). Phantom: Solana Devnet (`DEVNETSOL`, stablecoins: PYUSD, USDG) — enable via Settings > Developer Settings > Testnet mode.

**Get testnet tokens**: [sepoliafaucet.com](https://sepoliafaucet.com) (MetaMask) | [sepolia-faucet.pk910.de](https://sepolia-faucet.pk910.de) (Rainbow) | [faucet.solana.com](https://faucet.solana.com) (Phantom) | or ask your Mesh representative.

**Sandbox limitations**: Only Coinbase + legacy Binance available (other production exchanges not replicated). MMT not supported in sandbox. `Pending` webhook not guaranteed — transfers may go directly to `Succeeded` or `Failed`. Only MetaMask, Rainbow, and Phantom for wallet flows.

---

## Managing Sub-Clients

Register and manage sub-clients (child Mesh accounts) for B2B/PSP contexts where you serve multiple downstream merchants.

**When to use**: Only for platforms that embed Mesh in products for other businesses (PSPs, payment platforms). Single-product direct-to-consumer apps do not need sub-clients.

**Register via Dashboard**: Account > Link Configuration > Clients > Add a client. Fields: Business Legal Name, Display Name, Callback URL(s), icon upload.

**Register via API**: `POST` to Account Management API at `https://admin-api.meshconnect.com`. Requires separate Account Management API key (Dashboard: Account > API keys > Account management API keys) and IP allowlisting.

**Apply to user session**: Pass `subClientId` in Link Token request body alongside `userId` and `transferOptions`. Result: Link displays sub-client branding instead of top-level account branding. Omitting or using an incorrect `subClientId` silently falls back to top-level branding — no error thrown.

**Testing**: Register sandbox sub-clients separately from production. Use sandbox base URL for sandbox sessions.

---

## Manual Deposits

Mesh's QR code / copy-paste fallback deposit flow. Enabled by default for all deposit sessions. Supports SmartFunding and bridging.

**How it works**: User shown QR code + destination address + token/network + memo/tag if applicable. User initiates transfer from their own wallet or exchange. Mesh monitors blockchain. On detection -> `status: Succeeded` webhook (no `Pending` event first).

**Attribution logic**: Transfer attributed when correct token + network + address received, within 15 minutes of user viewing the QR code (under 30 min for BTC), and no concurrent connected-account deposit for that session.

**Appears in two scenarios**: (1) Primary option on landing page alongside "Connect account" (configurable). (2) Automatic fallback when direct connection fails or is unavailable.

**Not appropriate for**: Non-unique deposit addresses (unless using memo/tag), source-of-funds compliance requirements, exact payment amount requirements, or clients that must restrict to direct-only flows (contact Mesh to disable).

---

## Intelligent Catalog Filtering

Mesh automatically filters which integrations appear in Link during session initialization. No client action required — filtering is automatic.

**5 filter types**:
1. **Asset & Network Compatibility** (all clients): Only integrations supporting at least one `toAddresses` asset/network pair are shown.
2. **VASP ID Requirement** (Travel Rule — custodial platforms): If your business is a VASP, Mesh lacks your VASP ID, and user IP is in an affected country (EU countries + NZ, SG, KR) -> impacted exchanges hidden.
3. **Wallet Ownership Requirement** (Travel Rule — self-custody): If destination is a self-custody wallet, user IP in affected country, and transfer > 1000 EUR (or any amount in SG, HK, PH, KR) -> impacted exchanges hidden.
4. **Exchange Geography Restrictions** (all clients): Provider-level IP restrictions automatically applied. Examples: Binance hidden for US/Canada/Netherlands IPs; Robinhood shown only for US IPs.
5. **Use Case Restrictions** (varies): Some providers restrict transfers based on platform type and geography. Applied automatically.

**Action**: Contact your Mesh account manager to validate coverage for specific asset/network combinations or user regions, or to provide your VASP ID.

---

## Common Errors

**SDK domain not authorized** (CORS error or silent auth failure on load): Domain not on allowlist. Fix: Dashboard > Account > API keys > Access -> add all domains including `localhost:3000` and staging URLs. Takes effect within a few minutes.

**SDK iframe blocked by CSP** (blank grey box in embed area): CSP `frame-src` doesn't permit Mesh's iframe. Fix: add `*.meshconnect.com` to `frame-src`.

**Tron transfers blocked by CSP**: Add `https://tron.twnodes.com`, `https://trx.mytokenpocket.vip`, `https://api.trongrid.io` to `connect-src`.

**Ad-blocker interfering with OAuth** (spinner, OAuth never resolves): uBlock Origin, AdBlock Plus, or Brave blocking browser storage / OAuth redirects. Fix: user disables ad-blocker for your domain. Not officially supported.

**Link Token expired or already used** (error on `openLink()`): Tokens are 10 min, single-use. Fetching on page load means it can expire before the user clicks. Fix: fetch a new token on button click. Never cache or reuse tokens.

**Empty catalog / symbol not supported** (empty integration list or `toAddresses` validation error): ERC-20 contract address (e.g. `0xA0b8...`) passed as `symbol` instead of ticker (e.g. `USDC`). Fix: use ticker symbol. Key networkIds: Ethereum `e3c7fdd8-b1fc-4e51-85ae-bb276e075611`, Solana `0291810a-5947-424d-9a59-e88bb33e999d`, Base `aa883b03-120d-477c-a588-37c2afd3ca71`. Include multiple networkId/symbol pairs to maximize coverage.

**No eligible assets after connection** (`transferNoEligibleAssets` event fires): User's connected account holds no assets matching your `toAddresses`. Fix: broaden `toAddresses` to cover more asset/network combinations. Handle `transferNoEligibleAssets` and use `arrayOfTokensHeld` (each entry has `symbol`, `amount`, `amountInFiat`, `ineligibilityReason`) to show a helpful message. Sandbox: use `Mesh2` account to test empty-state UI.

**Webhook signature validation fails** (every event rejected with signature mismatch): Computing HMAC over parsed/re-serialized JSON instead of raw bytes, or `express.json()` middleware on webhook route destroys raw bytes, or using `.digest('hex')` instead of `.digest('base64')`. Fix: scope `express.raw({ type: '*/*' })` to the webhook route only, compute HMAC over `req.body` as a raw Buffer, Base64-encode the digest, compare to `X-Mesh-Signature-256` header.

**Duplicate webhook deliveries** (same event processed multiple times): Mesh delivers at-least-once. Non-2xx or slow responses trigger retries. Fix: use `EventId` as idempotency key (`EventId` is constant across retries; `Id` changes per delivery). Return 200 immediately, process async.

**OAuth blank screen or raw code in mobile WebView** (OAuth flow never completes in in-app browser): iOS `WKWebView` / Android `WebView` block `window.open()` by default. Fix: iOS -> implement `WKUIDelegate.webView(_:createWebViewWith:...)`. Android -> override `WebChromeClient.onCreateWindow()`. React Native -> use `react-native-inappbrowser-reborn` or `Linking.openURL`. Or ask Mesh about redirect-based OAuth (deep link / custom URL scheme).

**Transfer stays pending or finalizes as failed**: CEX processing delay is normal (up to 24h for Coinbase/Binance). `Pending` fires at exchange initiation; `Succeeded` fires on-chain. Check `minAmount` / `minAmountInFiat` in `toAddresses` to prevent below-minimum transfers. Verify destination address format is valid for the specified network. User 2FA timeout may cause the exchange to drop the transfer. Sandbox: `Pending` webhook not guaranteed. For stuck production transfers: contact Mesh with the `TransferId` from the webhook payload.