Cross-border payment widget.
A drop-in React component. User picks recipient + amount in their currency, widget fetches the cheapest source asset, generates a quote, and triggers wallet signing. Settles on Sera in one transaction.
A working <SeraPayWidget /> React component. Drop it on a page, pass recipient + amount + currency as props, and your users can settle international payments at on-chain cost (~$2 instead of $80+ via SWIFT).
Prerequisites
- Node 18+, npm or pnpm
- A React project (Next.js, Vite, CRA — anything)
- A wallet provider in your app: wagmi + RainbowKit recommended
sera-mcprunning somewhere your backend can talk to (typically as a subprocess of your Node API server, or as a hosted endpoint)
Architecture
Browser (React) Your Node API sera-mcp Sera Protocol
│ │ │ │
├── POST /api/quote ─────────▶│ │ │
│ ├── sera.pay_invoice ─────▶│ │
│ │ ├── /quote ─────────▶│
│ │ │◀── typed_data ─────┤
│ │◀── quote {uuid, ...} ────│ │
│◀── { quote, typed_data } ──┤ │ │
│ │ │ │
├── wallet.signTypedData ────▶│ (browser-only, key never leaves the wallet) │
│ │ │ │
├── POST /api/execute ───────▶│ │ │
│ (uuid + signature) ├── sera.execute_swap ────▶│ │
│ │ ├── /execute ───────▶│
│ │ │◀── trade_id ───────┤
│◀── { trade_id, tx_hash } ───┤ │ │
The browser never talks to sera-mcp directly — your backend brokers the calls. The wallet signs in the browser. The MCP never sees the key.
Steps
1Set up the backend route
Spawn sera-mcp as a subprocess of your Node server (or import its tool functions directly if you're embedding). Expose two routes: /api/quote and /api/execute.
// Pseudocode — wire to your framework's router (Next.js, Express, etc.)
import { spawnSeraMcp } from './mcpClient';
const sera = spawnSeraMcp({ env: { SERA_NETWORK: 'mainnet', POLICY_PRESET: 'standard' } });
export async function POST_quote(req) {
const { recipient, target_amount, target_currency } = req.body;
return await sera.call('pay_invoice', {
recipient, target_amount, target_currency
});
}
export async function POST_execute(req) {
const { quote_id, signature } = req.body;
return await sera.call('execute_swap', { quote_id, signature });
}
2Create the widget component
A single React component that handles the four states: idle, quoting, signing, executing.
import { useState } from 'react';
import { useSignTypedData } from 'wagmi';
type Props = { recipient: string; amount: number; currency: string };
export function SeraPayWidget({ recipient, amount, currency }: Props) {
const [state, setState] = useState<'idle'|'quoting'|'signing'|'executing'|'done'>('idle');
const [quote, setQuote] = useState<any>(null);
const [tradeId, setTradeId] = useState<string|null>(null);
const { signTypedDataAsync } = useSignTypedData();
async function pay() {
setState('quoting');
const q = await fetch('/api/quote', {
method: 'POST',
body: JSON.stringify({ recipient, target_amount: amount, target_currency: currency })
}).then(r => r.json());
setQuote(q);
setState('signing');
const sig = await signTypedDataAsync(q.typed_data);
setState('executing');
const r = await fetch('/api/execute', {
method: 'POST',
body: JSON.stringify({ quote_id: q.uuid, signature: sig })
}).then(r => r.json());
setTradeId(r.trade_id);
setState('done');
}
return (
<div className="sera-widget">
{state === 'idle' && (
<button onClick={pay}>
Send {amount} {currency} to {recipient.slice(0,6)}…
</button>
)}
{state === 'quoting' && 'Fetching cheapest source…'}
{state === 'signing' && 'Sign in your wallet'}
{state === 'executing' && 'Settling on Sera…'}
{state === 'done' && (
<span>Done. trade_id: <code>{tradeId}</code></span>
)}
</div>
);
}
3Use it in a page
import { SeraPayWidget } from '@/components/SeraPayWidget';
export default function Checkout() {
return (
<div>
<h1>Pay vendor in Malaysia</h1>
<SeraPayWidget
recipient="0xAa…vendor"
amount={5000}
currency="MYR"
/>
</div>
);
}
4Test in dry-run mode first
Set SERA_DRY_RUN=true in your backend env. Every execute_swap call returns a dry-run trace instead of an on-chain transaction. Run the full flow end-to-end without spending anything.
Once green, flip SERA_DRY_RUN=false and run a real low-notional ($10) settlement to verify on-chain.
5Show the user the cost saving (the marketing piece)
The widget should display the cost. Use sera.compare_to_external_fx to fetch a comparable TradFi rate and show a "$87 → $2.50" callout. This is what makes users understand they're getting a genuinely better deal.
const ext = await fetch('/api/compare?base=USD"e=MYR').then(r => r.json());
// ext.swift_estimate_usd vs quote.cost_usd
Reference repo
templates/web-chat in the sera-agents repo is a working starter that includes a wallet connector, a simple form, and the same backend ↔ MCP wiring. Fork it for the fastest path.
Common gotchas
- "Quote expired" — quotes from
get_quotehave a short expiry (typically 30s). If your user takes longer to sign, fetch a fresh quote. - Signed by wrong account — the wallet's connected address must equal the
makerfield in the typed data. Check both before signing. - Insufficient source token —
pay_invoiceassumes the user holds enough of the cheapest source asset. If they don't, fall back tofind_cheapest_settlement_pathwhich considers what they actually hold.
Next: Try the FX trading dashboard tutorial for read-only Sera consumption, or the x402-paid API tutorial if you'd rather skip MCP entirely.