# Yodl Pay Best-in-class QR payments with a 15-minute integration. # Yodl SDK Best-in-class QR payments with a 15-minute integration. Your users scan an everyday local QR code — PIX in Brazil, VietQR in Vietnam, GCash in the Philippines — and pay the merchant from their own wallet. The merchant receives their own currency, never crypto. Funds stay self-custodied until the moment of payment. :::tip[Start here] **Building it** — the [React Native quickstart](/sdk/react-native/quickstart) goes from install to a completed payment. **Seeing it first** — watch the [demo](/demo). **Deciding** — check [supported countries](/supported-countries) and [fees](/pricing). ::: ## Why Yodl QR payments are messy and hard to get right. Yodl has spent a year's worth of work integrating QR providers. They all come with endless quirks, failures, and constant fixes. Payment UX looks simple on the surface. But every single payment is like Demo Day: countless things can go wrong, leaving users unable to pay. Traumatized. Churn. Yodl SDK gives you best-in-class QR payments with a 15-minute integration: * **UI/UX included** — the SDK ships with a complete payment flow out of the box, guaranteeing consistent and fast payments. * **Processor aggregation** — we aggregate multiple processors, so you get the best rates, reliability, and feature set. * **Wallet-agnostic** — any EIP-1193 provider works: Privy, MetaMask, Coinbase Wallet, Rainbow, WalletConnect. * **Self-custody** — no deposits, no custodian. Funds leave the wallet at the moment of payment. ## The devil is in the detail Many "stablecoin orchestrators" claim to support a country's QR codes. In practice: * **QR payments are slow and not optimized for paying in a shop.** Churn. * **Not every QR code "flavor" is supported.** Users can't tell the difference, and 5% of QRs won't work. Churn. * **Failure and refund flows are an afterthought.** Payment failures go unhandled. Churn. * **Nobody tests on slow mobile networks.** Travel eSIM, slow SSL handshakes, servers far away. The loader spins 15 seconds just to resolve a QR. Churn. For most, QR code payments are just a ✅ on a feature table. For Yodl Pay QR codes are priority #1. ## What you actually integrate A provider and a component. The [React Native SDK](/sdk/react-native) ships the whole payment flow — localized per country, with no backend of yours involved. If you want payment records on your own backend, that is a separate, optional layer: see [backend integration](/integrations/overview). The payment flow works without it. # Supported countries Three tiers, and the difference matters for planning: **live** works today with no action from you, **on request** needs a conversation before your users can pay, and **coming soon** has no date attached. ## Live Available now. Nothing to enable — the SDK routes to these automatically. | Country | Payment methods | Processors | | --- | --- | --- | | Brazil | PIX | 2 live | | Argentina | Transferencias, Mercado Pago | 2 live | | Vietnam | VietQR, MoMo, VNPAY | 2 live | | Philippines | GCash, Maya (merchant), QR Ph | 1 live | More than one processor per country is deliberate — it is what lets us re-quote each payment and route to the cheapest all-in fee. See [fees & pricing](/pricing). :::note These four are live **without KYC**. KYC unlocks more countries, better rates and higher limits. ::: ## On request Supported, but enabled per integration rather than by default. * Peru * Colombia * Bolivia * Nigeria (PalmPay) :::tip If your users are in one of these, raise it during [onboarding](/integrations/onboarding) — enabling a country is an operator change on our side, not development work on yours. ::: ## Coming soon On the roadmap, without committed dates. Worth telling us if one of these is material to you, since demand affects ordering. * Hong Kong * Taiwan * Japan * Korea * Singapore * Cambodia * Nepal * India * Macau :::warning Don't ship a country from this list to users before we confirm it is live. `isPaymentQr` returns `false` for a QR we can't route, so the payment screen simply won't open — see [troubleshooting](/sdk/react-native/troubleshooting). ::: # Fees & pricing Yodl aggregates multiple processors per country and routes every payment to the one with the best **all-in fee**. There is no static fee table: you get the best available quote at the moment of payment. ## The all-in fee The all-in fee is what a payment actually costs, made up of: | Fee | Charged by | Details | | --- | --- | --- | | Processor fee | Processor | Percentage fee per payment | | Minimum / fixed service fee | Processor | Fee floor and/or flat fee, varies by processor | | FX quote | Processor | Exchange rate applied to convert into local currency | | Protocol fee | Yodl | 0.10%, currently waived | | Convenience fee | Wallet | Customizable by the wallet (30% cut to Yodl) | | Gas fee | Network | Bundled into the payment on L1, sponsored by Yodl on L2 | Yodl compares the all-in fee across processors for every single payment and picks the cheapest. :::note Two of those rows are yours to think about rather than ours. The **convenience fee** is your revenue lever — you set it, and we take 30% of it. The **protocol fee** is 0.10% and currently waived, so don't model it as zero indefinitely. ::: ## Example: Vietnam ### In an ideal world | | Processor fee | Minimum fee | FX quote | | ----------- | ------------- | ----------- | --------------- | | Processor 1 | 0.80% | $0.15 | $1 = 18,000 VND | | Processor 2 | 0.90% | none | $1 = 18,000 VND | With identical FX quotes, a $20 payment is cheaper on Processor 1 ($0.16 vs $0.18). On a $10 payment the minimum fee kicks in and Processor 2 wins ($0.09 vs $0.15). ### In reality FX quotes are never identical. They vary a lot, hour by hour, and the swings routinely dwarf the static fees. | | Processor fee | Minimum fee | FX quote | | ----------- | ------------- | ----------- | --------------- | | Processor 1 | 0.80% | $0.15 | $1 = 17,900 VND | | Processor 2 | 0.90% | none | $1 = 18,050 VND | For a 1,800,000 VND payment, Processor 1 costs $101.36 all-in, Processor 2 costs $100.62. The better FX quote beats the lower percentage fee. An hour later, the quotes may have flipped. That is why Yodl re-quotes every payment on the all-in fee instead of publishing a static fee table. :::tip The practical consequence: **don't hardcode a fee in your own UI.** Quote it from the payment flow at the moment of payment, or state a range. Any number you publish will be wrong within the hour. ::: ## Better rates with KYC Non-KYC payments are live in Brazil, Vietnam, Philippines, and Argentina — see [supported countries](/supported-countries). KYC unlocks more countries, better rates, and higher limits. # Demo See the Yodl SDK payment flow in action — scan, confirm, paid. This is the flow that ships with the SDK; you render one component. [Watch the Yodl SDK payment flow demo (MP4 video)](https://kuezuu6pdfndtxdd.public.blob.vercel-storage.com/docs/sdk-demo-h264.mp4) Ready to build it? The [React Native quickstart](/sdk/react-native/quickstart) gets you to a completed payment. # AI assistants These docs are built to be read by machines as well as people. If you are integrating the SDK with an AI coding assistant, point it here rather than pasting snippets — everything below is available without an API key or an account. ## On every page The **Ask AI** button at the bottom of each page opens the current page in ChatGPT or Claude, copies it as Markdown, links the raw Markdown, and — when you want the whole set — gives you the MCP server URL. ## Markdown routes Append `.md` to any page URL to get its source rather than the rendered HTML: ```txt https://docs.yodl.me/sdk/react-native/quickstart.md ``` Pages are also content-negotiated: a request without a browser `User-Agent` receives Markdown from the normal URL. So this returns Markdown, while your browser gets the rendered page: ```bash curl https://docs.yodl.me/sdk/react-native ``` ## llms.txt Two files, both generated on every build: | File | What it is | | --- | --- | | /llms.txt | An index — every page with its title, path and one-line description. ~4 KB | | /llms-full.txt | The entire documentation concatenated into one file. ~80 KB | Use the index to let a model choose what to read; use the full file when you want the whole corpus in context at once. ## MCP server The docs are also served over the [Model Context Protocol](https://modelcontextprotocol.io), so an assistant can search and read them on demand instead of holding everything in context. ```txt https://docs.yodl.me/api/mcp ``` ```bash [Claude Code] claude mcp add --transport http yodl-docs https://docs.yodl.me/api/mcp ``` ```json [Client config] { "mcpServers": { "yodl-docs": { "type": "http", "url": "https://docs.yodl.me/api/mcp" } } } ``` Three tools are exposed: | Tool | What it does | | --- | --- | | `list_pages` | Lists every documentation page with its title and path | | `read_page` | Reads one page, by `pagePath` | | `search_docs` | Searches the docs for a query string | ### What it can and cannot reach :::note This server exposes **documentation only**. It is read-only, needs no credentials, and can reach no account, wallet, payment or key material. There is nothing to scope and no token to leak. ::: That is worth stating plainly, because many vendor MCP servers do the opposite — they connect an assistant to live project data, which is why their docs open with token-scoping and "don't point this at production" warnings. This one is closer to a search index over public pages. It also does not serve the SDK's source code. The packages are published on npm ([`@yodlpay/react-native`](https://www.npmjs.com/package/@yodlpay/react-native), [`@yodlpay/react-native-eip-7702-provider`](https://www.npmjs.com/package/@yodlpay/react-native-eip-7702-provider)) and their types travel with them, which is the better source for an assistant writing integration code. ## One caveat An assistant that has read these docs still guesses. The SDK's config example is type-checked against the real package on every build, so what you see compiles — but a generated integration is not. Check anything an assistant writes against the [API reference](/sdk/react-native/api), and let the compiler decide. # React Native SDK [`@yodlpay/react-native`](https://www.npmjs.com/package/@yodlpay/react-native) adds QR payments to a React Native wallet app. The complete payment flow ships with the SDK — localized per country, with no backend of yours involved. Payments work with any token on any supported chain, so users don't have to hold a particular balance. Three drop-in components, all rendered inside a provider: | Component | What it does | | --- | --- | | `` | The payment flow for a scanned QR code | | `` | Hosted onboarding — SIWE, email capture, verification | | `` | The Yodl account view | ## What a payment looks like Your app scans and renders; the hosted UI resolves and quotes; the wallet signs. You never touch RPC or payment plumbing. ```mermaid sequenceDiagram autonumber actor U as User participant App as Your app participant Y as Yodl UI participant W as Wallet U->>App: Scans a merchant QR App->>App: isPaymentQr(qrData) App->>Y: Renders YodlPayment Y->>Y: Resolves across processors, quotes Y->>W: Signature or wallet_sendCalls W-->>Y: Signed Y-->>App: onTransactionSent(txHash) Y-->>App: onPaymentDetails(status) ``` The dashed returns are your callbacks — the SDK bridges messages from the UI, it does not fetch payment state itself. ## Requirements | Requirement | Version | | --- | --- | | React | `>= 18` | | React Native | `>= 0.72` | | Hermes engine | Enabled (default) | :::warning **Not Expo Go.** The SDK uses the native `react-native-webview` module, so it needs a [development build](https://docs.expo.dev/develop/development-builds/introduction/) (`npx expo run:ios` / `run:android`) or bare React Native. Expo Go will crash or render a blank screen. ::: Hermes is required because the SDK uses `BigInt`. It is on by default from React Native `0.70`. If you disabled it, re-enable it or add a `BigInt` polyfill. ## Install Install the SDK and its `react-native-webview` peer dependency, then rebuild the native app. :::code-group ```bash [Expo] npx expo install @yodlpay/react-native react-native-webview npx expo run:ios # or: npx expo run:android ``` ```bash [Bare React Native] npm install @yodlpay/react-native react-native-webview npx pod-install # iOS ``` ::: Expo's installer resolves the WebView version against your Expo SDK, which is why it is preferred over a plain `npm install` there. After installing, rebuild the app — that is the whole install. ## Where to go next * **[Quickstart](/sdk/react-native/quickstart)** — from install to a completed payment. * **[Signup flow](/sdk/react-native/signup)** — onboard users who don't have a Yodl account yet. * **[Theming](/sdk/react-native/theming)** — apply your palette to the hosted UI. * **[API reference](/sdk/react-native/api)** — every export, prop and callback. * **[Security model](/sdk/react-native/security)** — origin locking and what the bridge does and doesn't do. * **[Troubleshooting](/sdk/react-native/troubleshooting)** — symptoms and fixes. Wallets that can't batch calls natively may also want the [EIP-7702 provider](/sdk/eip-7702-provider) — read that page before installing it, since most integrations don't need it. # Quickstart By the end of this page a user can scan a merchant QR code in your app and complete a payment. **Before you start:** finish the [install](/sdk/react-native#install) and have a wallet SDK in place that gives you an EIP-1193 provider, the connected address, and the current chain id — Privy, MetaMask, Coinbase Wallet, Rainbow and WalletConnect all qualify. :::steps ##### Create the client once `createSdk` is called once for the whole app, not per screen. Put it in its own module and import the instance where you need it. ```ts twoslash [sdk.ts] import { createSdk } from '@yodlpay/react-native'; export const sdk = createSdk({ // Your registered integration address — this attributes payments to you. integrationAddress: '0x000000000000000000000000000000000000dEaD', // Chains the wallet is allowed to switch to. supportedChainIds: [1, 8453], }); ``` `integrationAddress` must be a non-empty `0x` address. If you don't have one yet, see [the integration address](/integrations/integration-address). ##### Guard the scanned QR `isPaymentQr` is a standalone helper — it needs no SDK instance, so you can call it before mounting anything. ```tsx import { isPaymentQr } from '@yodlpay/react-native'; function handleScan(scanned: string) { if (!isPaymentQr(scanned)) return; // not a Yodl QR — keep scanning setQrData(scanned); } ``` Skipping this check is the most common cause of a blank payment screen: `` renders nothing when `qrData` isn't a supported payment QR. ##### Render the payment flow Wrap the screen in `` and render `` inside it. ```tsx [PaymentScreen.tsx] import { YodlPayment, YodlProvider } from '@yodlpay/react-native'; import { sdk } from './sdk'; import { useWallet } from './wallet'; export function PaymentScreen({ qrData }: { qrData: string }) { const { provider, address, chainId } = useWallet(); return ( console.log('submitted:', txHash)} onError={(err) => console.error(err)} /> ); } ``` ::: That's a working payment. :::warning `address` and `chainId` must be the wallet's **live** values, passed straight from your wallet SDK's hooks — not fixed constants. The SDK pushes `accountsChanged` and `chainChanged` into the hosted UI as they change, so a stale `chainId` leaves the UI on the wrong chain. The `provider` is used only as the signing and RPC transport. ::: ## A fuller example Scan → pay → back, plus the dashboard and payment lifecycle events. ```tsx [PaymentScreen.tsx] import { useState } from 'react'; import { Button, Text, View } from 'react-native'; import { isPaymentQr, YodlDashboard, YodlPayment, YodlProvider } from '@yodlpay/react-native'; import { sdk } from './sdk'; import { useWallet } from './wallet'; import { QrScanner } from './QrScanner'; // your camera component, e.g. expo-camera type Screen = 'scan' | 'pay' | 'dashboard'; export function PaymentScreen() { const { provider, address, chainId, isConnected } = useWallet(); const [screen, setScreen] = useState('scan'); const [qrData, setQrData] = useState(null); if (!isConnected) return Connect a wallet to continue.; function handleScan(scanned: string) { if (!isPaymentQr(scanned)) return; setQrData(scanned); setScreen('pay'); } return ( {screen === 'scan' && (