Skip to content

About

This is the merchant's SDK for Web3Settle, a Non Custodial complete blockchain payment infrastructure built for merchants who value security, transparency, and low fees.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@web3settle/merchant-sdk

React component library for accepting crypto payments via Web3Settle. Drop in a provider and a pay button to take on-chain payments on Ethereum, Base, and TRON directly from your users' wallets — no custody, no card data, no server-side signing.

Chain cut (2026-09-24). Polygon was dropped permanently (decision D1) and the ./solana subpath was archived (decision D2). @web3settle/merchant-sdk/solana no longer exists, and chainId 137 is gone from the default chain catalog. See archive/solana/README.md and the CHANGELOG's Unreleased entry for the full list of removed exports.

Which chains are supported is stated in exactly one place: the platform's docs/CHAIN_MATRIX.md (workspace root of the web3settle-monorepo repo), mirrored contractually by docs/SOFTWARE_PRODUCT_DESCRIPTION.md §3.1. If this README and that table ever disagree, the table is right.

Distribution: each chain family lives behind a separate subpath export so the bundler only pulls in what you actually use.

Subpath What it brings Peer deps you must install
@web3settle/merchant-sdk EVM (Ethereum / Base) provider + button + modal + hooks wagmi, viem, @wagmi/core, @tanstack/react-query
@web3settle/merchant-sdk/tron TRON provider + button + modal + hooks (TronLink-backed) TronLink browser extension at runtime. The tronweb package is a peer for TypeScript types only — the SDK uses the extension's injected window.tronWeb
@web3settle/merchant-sdk/headless Framework-agnostic controllers (createPayButtonController, createWalletConnectController, createGasEstimateController) — V0.5.0 — for Vue / Svelte / vanilla JS callers None beyond your chain-stack peers above
@web3settle/merchant-sdk/wc <web3settle-pay-button> native HTMLElement built on top of the headless layer — V0.5.0 None beyond your chain-stack peers above

EVM-only consumers never pay the bundle cost of the TRON stack; TRON-only consumers never pull wagmi. Import only the subpaths you need.

How a payment reaches your order (read this first)

The gateway can only tell you who paid if the pay-in carries a payment reference. The flow is four steps, and only the second happens in the browser:

your server ──POST /api/payment/create (X-Api-Key)──▶ gateway      → { requestId, paymentReference, status: "pending" }
browser     ──SDK: payInNativeWithReference / payInTokenWithReference(paymentReference)──▶ merchant contract (MerchantPayIn ≥ V3.2.3)
gateway     ──detects the deposit, links it to the request──▶ status: pending → processing → confirmed
gateway     ──POST payment.confirmed (X-Webhook-Signature: v1=…)──▶ your webhook → credit data.metadata.userId
  1. Create the payment on your server — never in the browser; it needs your storefront API key. Put what you need back on the webhook into metadata (the webhook has no userId field of its own). amount is in whole token units of the asset you will be paid in (e.g. 0.01 ETH or 9.99 USDC), not USD: the gateway only links a deposit of at least that amount, in that asset, on that network, into that storefront's contract.

    // server-side (Node)
    const res = await fetch(`${GATEWAY}/api/payment/create`, {
      method: 'POST',
      headers: { 'content-type': 'application/json', 'x-api-key': process.env.WEB3SETTLE_API_KEY!, 'idempotency-key': orderId },
      body: JSON.stringify({
        network: 1,                   // BlockchainNetwork ordinal: Ethereum 0, Base 1, Tron 2
        toAddress: contractAddress,   // GET /api/payment/deposit-address?network=Base
        amount: 0.01,                 // whole units of the token below (here: ETH)
        tokenAddress: undefined,      // ERC-20 address, or omit for the native coin
        metadata: { userId, orderId },
      }),
    });
    const { requestId, paymentReference } = await res.json(); // paymentReference: bytes32 hex
  2. Pay with the reference in the browser. Either hand it to the hook yourself…

    const { startPayment } = usePayment();
    await startPayment(amountUsd, chain, token, { atomicAmount, paymentReference });

    …or let the modal ask your backend at click time through createPayment (it passes the exact token, chain and amount about to be sent, plus userId):

    <Web3SettleProvider config={{ apiBaseUrl, storefrontId,
      createPayment: async (ctx) => (await fetch('/api/checkout', { method: 'POST', body: JSON.stringify(ctx) })).json(),
    }}>

    Attribution is the default and is enforced: without a valid reference the SDK refuses before the wallet is prompted (MissingPaymentReferenceError). The plain payInNative() / payInToken() still exist behind the explicit mode: 'unattributed' opt-in — such a deposit is detected and settled, but reaches your webhook with no paymentRequestId and no metadata.

  3. Poll or wait. GET /api/payment/status/{requestId} (server-side, same API key) goes pending → processing (deposit seen, with txHash and live confirmations) → confirmed (required depth reached) — or failed if a reorg removed the deposit.

  4. Credit on the webhook, not on the browser's success callback. Verify X-Webhook-Signature: v1=hex(HMAC-SHA256(secret, "{X-Webhook-Timestamp}.{rawBody}")) over the raw body, reject stale timestamps, ignore test: true, dedupe on data.txHash, and credit data.metadata.userId. data.amountPaid is what arrived (gross); data.amountInvoiced is what the request asked for — credit min(amountPaid, amountInvoiced) unless you handle overpayment.

Needs a merchant contract at MerchantPayIn V3.2.3 or later; a V3.2.2 contract has no …WithReference functions and the wallet's gas estimation fails. The TRON subpath (0.7.0) follows the same contract: useTronPayment().startPayment(…, { paymentReference }) sends payInNativeWithReference / payInTokenWithReference, TronTopUpModal asks your Web3SettleConfig.createPayment for the reference at Pay time, and attribution is the default there too.

Known limitation (0.6.0). WEB3SETTLE_PAYMENT_CONFIG_PUBKEY_PRIMARY is still the all-zero development placeholder, so the signed GET /payment-config never verifies against a real gateway and Web3SettlePayButton / Web3SettleTopUpModal cannot load their chain list. Until the real key ships in an SDK release, drive payments with usePayment and your own ChainConfig (what merchant-integration-demo does). Tracked in the workspace humanpending.md.

Features

  • Three chains across two stacks: Ethereum, Base (wagmi + viem) and TRON (TronLink)
  • Unified PaymentPipeline interface so both stacks present the same quoteAmount → needsApproval → approve → execute → waitForReceipt surface
  • Native currency and fungible-token payments on every chain
  • EVM: ERC-20 approval flow with exact-amount allowance (never unlimited). EIP-712 permit (V0.5.0 / segment 14.6) — when the token implements EIP-2612 the SDK signs the typed-data permit and submits permit(...) directly, saving the user one popup and ~$0.50 of gas.
  • TRON: TRC-20 approve + pay, SafeTRC20-aware for non-return-value tokens like USDT-TRON
  • Built-in wallet connection per chain (injected + WalletConnect on EVM; TronLink on TRON)
  • Real-time transaction status tracking with reorg-aware confirmation counts
  • Gas estimator (V0.5.0 / segment 14.1) — estimateEvmGas, estimateTronGas — single GasEstimate shape across both stacks; the modal renders a ≈ $X network-fee badge
  • Telemetry breadcrumbs (V0.5.0 / segment 14.2) — opt-in onTelemetry callback emits a privacy-redacted event per failed pay-in (no plain addresses, no amounts; PII-redacted message ≤240 chars)
  • Headless layer + Web Components (V0.5.0 / segment 14.5) — @web3settle/merchant-sdk/headless and @web3settle/merchant-sdk/wc (<web3settle-pay-button> native HTMLElement) for Vue / Svelte / vanilla JS callers
  • CoinGecko price feeds with in-memory caching + stale-while-revalidate fallback
  • Dark theme glassmorphism UI with CSS-variable hooks for theming
  • Zod-validated API responses at every boundary
  • Tree-shakeable ES modules + CommonJS output, one subpath per chain family
  • Full TypeScript type exports
  • i18n via i18next — English and Brazilian Portuguese shipped; straightforward extension to any locale; separate tron.* key namespace
  • Accessibility: role-dialog modal with focus trap, ESC close, screen-reader labels

Install

npm install @web3settle/merchant-sdk wagmi viem @tanstack/react-query @wagmi/core

All four are peer dependencies — they must live in your app's own node_modules tree. The SDK itself adds only zod at runtime.

Peer dependencies

Package Range
react ^18.0.0 || ^19.0.0
react-dom ^18.0.0 || ^19.0.0
wagmi ^3.6.16
viem ^2.52.2
@wagmi/core ^3.5.0 (optional)
@tanstack/react-query ^5.101.0

Quick start

1. Wrap your app with the provider

import { Web3SettleProvider } from '@web3settle/merchant-sdk';
import '@web3settle/merchant-sdk/styles.css';

function App() {
  return (
    <Web3SettleProvider
      config={{
        apiBaseUrl: 'https://api.yoursite.com',
        storefrontId: 'your-storefront-uuid',
        // Your backend creates the gateway payment and returns { paymentReference, requestId }.
        createPayment: (ctx) => fetch('/api/checkout', { method: 'POST', body: JSON.stringify(ctx) }).then((r) => r.json()),
        // Browser-side "tx mined" signal only — credit the user from the signed webhook.
        onSuccess: (session) => console.log('Payment sent:', session.id, session.txHash),
        onError: (error) => console.error('Payment failed:', error),
      }}
      walletConnectProjectId="your-wc-project-id" // optional
    >
      <YourApp />
    </Web3SettleProvider>
  );
}

2. Add a pay button

import { Web3SettlePayButton } from '@web3settle/merchant-sdk';

export function Checkout() {
  return (
    <Web3SettlePayButton
      amount={29.99}
      label="Pay with Crypto"
      variant="primary"
      size="lg"
    />
  );
}

3. Or open the modal directly

import { useState } from 'react';
import { Web3SettleTopUpModal } from '@web3settle/merchant-sdk';

export function TopUp() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsOpen(true)}>Top Up</button>
      <Web3SettleTopUpModal
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        amount={50}
        userId="user-123"
      />
    </>
  );
}

TRON quick start

The TRON subpath uses the user's TronLink extension at runtime. No build-time TronWeb dependency is required.

npm install @web3settle/merchant-sdk @tanstack/react-query
# tronweb is a peer for TypeScript types only; no runtime install required if you don't need the types in your own code.
import { TronWeb3SettleProvider, TronPayButton } from '@web3settle/merchant-sdk/tron';
import '@web3settle/merchant-sdk/styles.css';

export function App() {
  return (
    <TronWeb3SettleProvider
      config={{ apiBaseUrl: 'https://api.yoursite.com', storefrontId: 'uuid' }}
    >
      <TronPayButton amount={29.99} label="Pay with TRX or USDT-TRON" />
    </TronWeb3SettleProvider>
  );
}

Give the provider the same createPayment callback as the EVM one: the modal calls it with the exact chain / token / amount about to be sent, your server creates the gateway payment (POST /api/payment/create) and returns its paymentReference, and only then is TronLink prompted — with payInNativeWithReference / payInTokenWithReference (MerchantPayIn V3.2.3). Without a createPayment the modal refuses to pay; payInMode: 'unattributed' is the explicit opt-out.

For a custom flow, useTronPayment() mirrors usePayment():

const { startPayment } = useTronPayment();
await startPayment(0, chain, 'native', {
  paymentReference: instruction.paymentReference, // from your backend's POST /api/payment/create
  atomicAmount: instruction.amountAtomic,         // sun / raw TRC-20 units; skips the SDK's own quote
});

useTronPipeline() gives you the TronPaymentPipeline for custom flows (execute(chain, token, amount, { paymentReference })). The provider surfaces { address, connected, connecting, error } via useTronWeb3SettleContext().wallet, and exposes connect() / disconnect() for manual control.

Supporting multiple chains in the same app

EVM and TRON each need their own provider — they use different wallet stacks and can't share a single Web3SettleProvider. Nest them, or pick at render time:

import { Web3SettleProvider, Web3SettlePayButton } from '@web3settle/merchant-sdk';
import { TronWeb3SettleProvider, TronPayButton } from '@web3settle/merchant-sdk/tron';

export function MultiChainCheckout({ chain, amount }) {
  if (chain === 'tron') {
    return (
      <TronWeb3SettleProvider config={…}>
        <TronPayButton amount={amount} />
      </TronWeb3SettleProvider>
    );
  }
  return (
    <Web3SettleProvider config={…}>
      <Web3SettlePayButton amount={amount} />
    </Web3SettleProvider>
  );
}

Both pipelines implement the same PaymentPipeline interface exported from the root:

import type { PaymentPipeline, PaymentReceipt, PaymentErrorKind } from '@web3settle/merchant-sdk';

so you can build your own chain-agnostic UI if the default modals don't suit.

API reference

Components

Component Description
Web3SettleProvider Context provider wrapping WagmiProvider, QueryClientProvider, and SDK config. Required root.
Web3SettlePayButton Styled button that opens the payment modal. Three variants × three sizes.
Web3SettleTopUpModal Full multi-step payment flow modal (amount → wallet → token → review → status).
ChainSelector Network selection with icons and token tooltips. role="radiogroup" + keyboard-accessible.
TokenSelector Token selection with live balance and USD display. Falls back gracefully if RPC is unreachable.
TransactionStatus Animated transaction progress indicator (sending → confirming → success) + explorer link.
WalletConnect Wallet connection UI listing every configured wagmi connector.

Hooks

Hook Description
useWeb3Settle() Access SDK config + cached payment configuration loaded from the API. Exposes refetch().
usePayment() Full payment lifecycle: chain switch, USD→token conversion, approval, pay-in, receipt wait. Returns { status, txHash, error, startPayment, reset }. startPayment(amount, chain, token, { paymentReference }) pays with payIn*WithReference; without a reference it refuses unless mode: 'unattributed'.
useWallet() Wagmi-wrapped convenience hook: { address, isConnected, chainId, balance, balanceSymbol, displayAddress, connectors, connect, disconnect, error }.

Low-level utilities

Useful if you want to drive payments without the UI, or integrate with an existing checkout.

import {
  // Contract interaction
  executePayInNativeWithReference, // attributed (V3.2.3) — use these for orders
  executePayInTokenWithReference,
  buildPayInCall,                  // pure: { data, value } for either form
  executePayInNative,              // unattributed — no paymentRequestId/metadata on the webhook
  executePayInToken,
  // Payment references
  isPaymentReference,
  resolveAttribution,
  MissingPaymentReferenceError,
  InvalidPaymentReferenceError,
  approveToken,
  checkAllowance,
  getTokenBalance,
  getTokenDecimals,
  waitForReceipt,
  parseTokenAmount,
  // Price feeds
  getNativeTokenPrice,
  getTokenPrice,
  usdToNativeAmount,
  usdToTokenAmount,
  clearPriceCache,
  // API client
  Web3SettleApiClient,
  Web3SettleApiError,
} from '@web3settle/merchant-sdk';

Constants, schemas, and types

import {
  // Constants
  PAYMENT_CONTRACT_ABI,
  ERC20_ABI,
  DEFAULT_CHAINS,
  CHAIN_ICONS,
  COINGECKO_CHAIN_IDS,
  PRICE_CACHE_TTL_MS,
  NATIVE_TOKEN_SENTINEL,
  // Zod schemas (validate at runtime)
  TokenConfigSchema,
  ChainConfigSchema,
  PaymentConfigSchema,
  PaymentSessionSchema,
  Web3SettleConfigSchema,
  // Enums
  PaymentStatus,
} from '@web3settle/merchant-sdk';

// TypeScript types (erased at build time)
import type {
  TokenConfig,
  ChainConfig,
  PaymentConfig,
  PaymentSession,
  CreatePaymentContext,
  CreatedPayment,
  StartPaymentOptions,
  PaymentReference,
  PayInMode,
  Web3SettleConfig,
  PayButtonProps,
  TopUpModalProps,
  ChainSelectorProps,
  TokenSelectorProps,
  TransactionStatusProps,
  WalletConnectProps,
  ButtonVariant,
  ButtonSize,
  TokenSelection,
} from '@web3settle/merchant-sdk';

Configuration

Web3SettleProvider props

Prop Type Required Description
config Web3SettleConfig Yes See table below.
children ReactNode Yes Your app.
walletConnectProjectId string No WalletConnect v2 project ID from cloud.walletconnect.com. Omit to use injected connectors only.
queryClient QueryClient No Reuse your existing React Query client. The SDK creates one if omitted.

Web3SettleConfig

Property Type Required Description
apiBaseUrl string Yes HTTPS URL of the Web3Settle backend (MerchantPaymentApi).
storefrontId string Yes Your storefront UUID from the back office.
theme 'dark' | 'light' No UI theme. Default 'dark'. (Only dark ships today; light is reserved.)
createPayment (ctx: CreatePaymentContext) => Promise<{ paymentReference, requestId? }> Yes, unless payInMode: 'unattributed' Called on Pay, before the wallet prompt. Your backend creates the gateway payment for exactly ctx.amount of ctx.token and returns its paymentReference.
payInMode 'attributed' | 'unattributed' No Default 'attributed'. 'unattributed' sends the plain payIn* (no order linkage).
onSuccess (session: PaymentSession) => void No Fires after the tx is mined at the SDK's receipt depth. session.id is the requestId from createPayment. Not a credit signal — credit from the webhook.
onError (error: Error) => void No Fires on any unrecoverable failure (revert, user rejection, network error).

PayButtonProps

Property Type Default Description
amount number — Payment amount in USD.
label string "Pay $X.XX" Button label.
variant 'primary' | 'outline' | 'ghost' 'primary' Visual style.
size 'sm' | 'md' | 'lg' 'md' Button size.
disabled boolean false Disable the button.
className string — Additional CSS classes (merged after the variant/size classes).

Internationalisation (i18n)

The SDK ships with translations in English (en) and Brazilian Portuguese (pt-BR) and uses i18next + react-i18next under the hood. Strings are namespaced under web3settle-sdk so they never collide with your application's own translations.

Turn it on

There are two modes, pick whichever matches how your app already handles i18n.

Mode A — you already use i18next. Merge the SDK's resource bundles into your existing instance:

// src/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { addSdkResourcesTo } from '@web3settle/merchant-sdk';

void i18n
  .use(initReactI18next)
  .init({
    lng: 'pt-BR',
    fallbackLng: 'en',
    interpolation: { escapeValue: false },
    resources: {
      en: { translation: { /* your app strings */ } },
      'pt-BR': { translation: { /* your app strings */ } },
    },
  })
  .then(() => {
    // Adds `web3settle-sdk` namespace bundles for every locale the SDK ships.
    addSdkResourcesTo(i18n);
  });

export default i18n;

The SDK components pick the language from your i18n.language. Changing it with i18n.changeLanguage('pt-BR') updates the SDK UI in the same frame.

Mode B — you don't use i18next in your app. Let the SDK create a standalone instance:

import { ensureSdkI18n } from '@web3settle/merchant-sdk';

const sdkI18n = ensureSdkI18n();
await sdkI18n.changeLanguage('pt-BR');

The SDK components will use this instance automatically.

Supported locales at a glance

import { SUPPORTED_LOCALES, LOCALE_LABELS, SDK_NAMESPACE } from '@web3settle/merchant-sdk';

// SUPPORTED_LOCALES = ['en', 'pt-BR'] as const
// LOCALE_LABELS     = { en: 'English', 'pt-BR': 'Português (Brasil)' }
// SDK_NAMESPACE     = 'web3settle-sdk'

Adding a new language

The SDK will happily translate into any language you register with it. The work is just a JSON file and three lines of glue code in your app.

Step 1 — copy one of the shipped locale files as a starting point. The shape is stable across versions; pick en.json (authoritative) or pt-BR.json (reference for gendered / length-sensitive translations).

# Grab the canonical English pack from the installed package:
cp node_modules/@web3settle/merchant-sdk/src/i18n/locales/en.json \
   src/locales/web3settle-sdk.es-ES.json

Open the file and translate every leaf string, leaving the keys unchanged. A complete pack looks like this:

// src/locales/web3settle-sdk.es-ES.json
{
  "payButton": {
    "default": "Pagar {{amount}} USD",
    "label": "Pagar con cripto"
  },
  "modal": {
    "title": "Recarga",
    "enterAmount": "Introduce el importe",
    "connectWallet": "Conectar monedero",
    "selectPayment": "Selecciona el pago",
    "reviewPayment": "Revisar pago",
    "processing": "Procesando",
    "complete": "Completado",
    "failed": "Error",
    "amountUsd": "Importe (USD)",
    "continue": "Continuar",
    "reviewButton": "Revisar pago",
    "confirmButton": "Confirmar pago",
    "done": "Hecho",
    "tryAgain": "Intentar de nuevo",
    "close": "Cerrar",
    "back": "Atrás"
  },
  "wallet": {
    "connect": "Conectar monedero",
    "connecting": "Conectando…",
    "connected": "Conectado",
    "disconnect": "Desconectar",
    "rejected": "Conexión rechazada por el usuario"
  },
  "chain": {
    "select": "Selecciona red",
    "tokensCount_one": "{{count}} token",
    "tokensCount_other": "{{count}} tokens"
  },
  "token": {
    "select": "Selecciona token",
    "native": "Nativo"
  },
  "status": {
    "sending": "Enviando transacción",
    "confirming": "Confirmando on-chain",
    "success": "Pago confirmado",
    "txFailed": "Transacción fallida",
    "paymentSuccessful": "Pago realizado",
    "paymentSuccessBody": "Tu transacción ha sido confirmada on-chain.",
    "viewOnExplorer": "Ver en el explorador",
    "viewTxOnExplorer": "Ver transacción en el explorador",
    "unexpectedError": "Ha ocurrido un error inesperado"
  },
  "errors": {
    "rejected": "Transacción rechazada por el usuario",
    "insufficientFunds": "Saldo insuficiente para esta transacción",
    "noWallet": "Monedero no conectado",
    "tokenNotFound": "Token {{token}} no encontrado en la configuración de red",
    "reverted": "Transacción revertida on-chain"
  },
  "branding": {
    "poweredBy": "Desarrollado por",
    "brand": "Web3Settle"
  }
}

Step 2 — register the pack with the SDK's i18n instance.

If you use Mode A (your own i18next):

import i18n from './i18n';
import { SDK_NAMESPACE } from '@web3settle/merchant-sdk';
import esES from './locales/web3settle-sdk.es-ES.json';

// Tell i18next that 'es-ES' exists and what its SDK pack looks like.
i18n.addResourceBundle('es-ES', SDK_NAMESPACE, esES, /* deep */ true, /* overwrite */ false);

// Let users switch to it:
await i18n.changeLanguage('es-ES');

If you use Mode B (SDK-owned instance):

import { ensureSdkI18n, SDK_NAMESPACE } from '@web3settle/merchant-sdk';
import esES from './locales/web3settle-sdk.es-ES.json';

const sdkI18n = ensureSdkI18n();
sdkI18n.addResourceBundle('es-ES', SDK_NAMESPACE, esES, true, false);
await sdkI18n.changeLanguage('es-ES');

Step 3 — (optional) advertise the locale in your language switcher.

SUPPORTED_LOCALES and LOCALE_LABELS exported by the SDK are not mutable — they describe the bundles the SDK ships with. If you want your own language switcher to list Spanish as well, maintain your own array in your app and include whatever languages you've added via addResourceBundle.

const ALL_LOCALES = [
  { code: 'en',    label: 'English' },
  { code: 'pt-BR', label: 'Português (Brasil)' },
  { code: 'es-ES', label: 'Español (España)' },
];

Keeping translations in sync across releases

When we add a new string in a minor version, the English entry is authoritative. Missing keys in custom locale files fall back to English automatically — nothing breaks, but the un-translated string will appear in English until you copy the new key over. We recommend running a diff against node_modules/@web3settle/merchant-sdk/src/i18n/locales/en.json after every version bump.

Interpolation and pluralisation

Translations use i18next interpolation syntax for substitution and the standard _one / _other suffixes for plurals:

"chain": {
  "tokensCount_one":   "{{count}} token",
  "tokensCount_other": "{{count}} tokens"
}

Follow CLDR plural categories for the languages you add (some languages need _few, _many, _zero, etc.).

Right-to-left languages

Visual RTL support (dir="rtl") is not applied automatically — your host app's root element should set dir based on the active locale. The SDK's layout uses logical properties (ms-, me-, start, end) wherever Tailwind supports them, so it adapts correctly when the host sets dir="rtl".

Supported chains

Chain Stack Native Default whitelist SDK subpath
Ethereum EVM / wagmi ETH USDC, USDT root
Base EVM / wagmi ETH USDC root
TRON mainnet TronLink TRX TRC-20 (incl. USDT-TRON via SafeTRC20) /tron

All three use the same MerchantPayIn V3.0 contract model — immutable commission set at initialize, token whitelist managed by the admin role, no renegotiation path.

Removed 2026-09-24 (chain cut): Polygon (decision D1) and Solana (decision D2). The product's GTM target is four mainnets; the fourth is an open founder decision (D4a) tracked in the workspace-root humanpending.md. Adding a fourth EVM chain here is a config change only — one entry in DEFAULT_CHAINS, CHAIN_ICONS, COINGECKO_CHAIN_IDS, KNOWN_CONTRACT_ADDRESSES, DEFAULT_CONFIRMATION_THRESHOLDS, CHAIN_FAMILY_REGISTRY, DEFAULT_SECONDS_TO_FINALITY, plus the wagmi chain list in Web3SettleProvider.

Security notes

  • All API responses are validated with Zod schemas before use.
  • Signed PaymentConfig (V0.5+). GET /payment-config returns { data, signedAt, signature }; the SDK refuses to build calldata unless the Ed25519 signature verifies against WEB3SETTLE_PAYMENT_CONFIG_PUBKEY_PRIMARY (or _SECONDARY during a rotation overlap) over signedAt + canonical_json(data). The constants are baked into the SDK at release time so a poisoned-DNS or CDN-edge MITM that swaps the contract address cannot be silently honoured. The .well-known/web3settle-config-pubkey endpoint mirrors the constants for out-of-band drift checks.
  • Contract allowlist. The SDK refuses to call payIn* against any address that is neither in the baked-in KNOWN_CONTRACT_ADDRESSES nor explicitly elevated by the signed allowedContractAddresses map for the storefront. New canonical addresses require an SDK release.
  • ABI version handshake. The signed payload carries contractAbiVersion. The SDK fails closed when it sees a revision not in SUPPORTED_ABI_VERSIONS.
  • Permit allowlist. permit: 'auto' falls back to approve() for any token whose (name, version, chainId, verifyingContract) quadruple is not in the SDK's baked-in KNOWN_PERMIT_TOKENS (0.6.0: USDC Ethereum, DAI Ethereum, USDC Base — verified on-chain, see docs/PERMIT_ALLOWLIST.md). A token whose PERMIT_TYPEHASH is not EIP-2612's (DAI) is treated as permit-unsupported and also uses approve(). permit: 'require' raises UnknownPermitTokenError on unknown tokens. To add a token, compute permitDomainKey(name, version, chainId, verifyingContract) and PR the resulting hex digest into core/config.ts; the addition ships in the next SDK release.
  • Salted telemetry digests. walletDigest is salted by (storefrontId, dayUtc) so two storefronts of the same wallet cannot be cross-joined and the same shop's digests rotate every UTC day.
  • ERC-20 approvals request only the exact amount needed — never unlimited.
  • Transaction receipts are verified for status === 'success'; reverts surface as an Error.
  • Wallet connections use standard EIP-1193 providers via wagmi; the SDK does not read or persist private keys.
  • Web3SettleApiClient validates storefrontId as a UUID at construction time and builds URLs via the URL constructor (no string concat). It has no payment-creation call: that needs your API key and belongs on your server.
  • Payment references are public. Anyone can pay any contract with any reference; the gateway only links a deposit that could settle the request (same merchant, storefront, network, asset; at least the amount; request still pending). Never treat "a tx with my reference exists" as payment — wait for confirmed / the signed webhook.
  • Modal: role="dialog" + aria-modal + focus restoration + ESC-key close. Click-outside closes on mouse but the Escape handler is always present for keyboard-only users.

Adding a token to the permit allowlist

The permit: 'auto' path silently signs EIP-2612 permits only for tokens whose EIP-712 domain quadruple is on the SDK's allowlist. To add one:

import { permitDomainKey } from '@web3settle/merchant-sdk';
console.log(permitDomainKey('USD Coin', '2', 1, '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'));
// → "<64-char hex>"

Read name() / version() from the token itself and check its DOMAIN_SEPARATOR() against the quadruple first — docs/PERMIT_ALLOWLIST.md has the script. Open a PR against merchant-sdk/src/core/config.ts adding the digest to KNOWN_PERMIT_TOKENS. Include in the PR body the source you used to verify the contract address (Etherscan label, official docs link). The merge ships in the next SDK release; backend deploys alone cannot expand the set.

Browser support

Modern evergreen browsers with ES2020 support (Chrome 94+, Firefox 93+, Safari 15+, Edge 94+).

Development

npm install
npm run dev          # Build with watch
npm run build        # Production build (tsc + Vite lib)
npm run test         # Run Vitest (12 test files, 150 tests)
npm run typecheck    # tsc --noEmit
npm run lint         # ESLint (flat config, strict type-checked)
npm run audit:ci     # Fail on high/critical vulns

Bundle output

Built artefacts in dist/:

File Purpose
index.js ESM entry point.
index.cjs CommonJS entry point.
index.d.ts Rolled-up TypeScript declarations.
styles.css Tailwind-compiled stylesheet. Import separately via @web3settle/merchant-sdk/styles.css.
styles.js / styles.cjs 40-byte side-effect entry that imports styles.css (used by tooling).

Source maps accompany every .js / .cjs output.

License

Proprietary — Copyright © 2026 Web3Settle.com. See LICENSE for the full terms. Redistribution, sublicensing, and embedding this SDK into third-party products requires written permission from Web3Settle.com.

About

This is the merchant's SDK for Web3Settle, a Non Custodial complete blockchain payment infrastructure built for merchants who value security, transparency, and low fees.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages