What we’re building
A Next.js app that connects Dynamic’s embedded wallets to MoneyGram Ramps, letting users convert USDC to cash at any MoneyGram location worldwide. The integration covers three chains — Base, Ethereum, and Solana — using a single email-based authentication flow.How it works
MoneyGram Ramps is a full-screen iframe that handles the entire offramp flow internally: country selection, amount entry, live quotes, KYC, fraud disclosure, and transaction confirmation. Your app’s only responsibilities are:- Respond to
RAMPS_CONFIG— send wallet address, chain, and API key when the widget loads - Respond to
RAMPS_CHECK_BALANCE— fetch the user’s on-chain USDC balance and return it - Respond to
RAMPS_SIGN_TRANSACTION— sign and broadcast the USDC transfer when the user confirms
window.postMessage. The MoneyGram REST API is called from inside the iframe — you never call it directly from your app.
Authentication uses Dynamic’s JS SDK. On successful sign-in, Dynamic automatically provisions embedded wallets for both EVM and Solana, so users have a wallet address on every supported chain without installing any extensions.
Building the application
Project setup
Scaffold a Next.js app and follow the JavaScript quickstart. This example uses@dynamic-labs-sdk/client with EVM and Solana extensions for signing, and @dynamic-labs-sdk/react-hooks for reactive auth state (DynamicProvider, useUser, useGetWalletAccounts).
Dashboard: Enable EVM and Solana embedded wallets under Chains & Networks. Enable Email OTP and Google under Sign-in Methods. Under Security → Allowed Origins, add your local origin (for example
http://localhost:3000).Install dependencies
Configure environment variables
.env.local
lib/env.ts
Initialize Dynamic
Create the Dynamic client with EVM and Solana extensions. Both extensions must be registered immediately aftercreateDynamicClient(). The client auto-initializes — no explicit initializeClient() call is needed.
lib/dynamic.ts
Configure wallet context
Providers wraps the app in DynamicProvider from @dynamic-labs-sdk/react-hooks. Components read auth state directly via useUser() and useGetWalletAccounts() — no custom context or useState for auth state needed:
lib/providers.tsx
components/ramp-app.tsx
Configure chains
Define aMgChain union type that maps directly to the chain identifiers MoneyGram expects. Each entry includes the USDC contract address (or SPL mint) and the chain-specific config needed for balance reads and transaction signing.
lib/chains.ts
Authenticate and create wallets
Use theuseWallet() context in your component to check sign-in state. For the auth form, call sendEmailOTP and verifyOTP directly:
components/ramp-app.tsx
createWaasWalletAccounts provisions non-custodial embedded wallets for both EVM and Solana in a single call. Because the component is inside DynamicProvider, useGetWalletAccounts() re-renders automatically when wallets are ready — no manual event subscription needed.
Fetch USDC balance
Before opening the widget, and again after a successful transaction, fetch the user’s on-chain USDC balance. The implementation branches on chain type:- EVM: calls
balanceOf(address)on the USDC contract via viem’sreadContract - Solana: resolves the Associated Token Account (ATA) for the user’s address and queries its token balance
lib/balance.ts
formatUnits(raw, 6) on EVM and uiAmountString on Solana both return human-readable values.
Handle the postMessage protocol
The widget communicates with your app viawindow.postMessage. Set up a single message event listener when the widget opens. Always validate event.origin before acting on a message, and always pass WIDGET_ORIGIN as the target origin when posting back — never '*'.
The full message sequence is:
components/cash-pickup-widget.tsx
The
message listener is registered once when open becomes true. If you close over state directly, the handler captures stale values. The example syncs selectedChain, walletAccounts, onClose, and onSuccess into refs so the event handler always reads the current values without needing to re-register.
RAMPS_CONFIG is the only message needed to start the widget flow. Do not send RAMPS_INIT or RAMPS_OPEN — these are not valid message types and will leave the widget frozen.Always include
walletAddress in RAMPS_BALANCE_RESULT. The widget uses it to look up an existing MoneyGram profile and pre-fill the KYC form for returning users.Sign and broadcast the USDC transfer
When the widget firesRAMPS_SIGN_TRANSACTION, it provides the exact recipient address and amount. Your app signs and broadcasts the transfer, then responds with the transaction hash.
The signing path differs by chain type:
EVM (Base and Ethereum)
Use Dynamic’s createWalletClientForWalletAccount to get a viem wallet client for the user’s embedded wallet, then encode an ERC-20 transfer call and send it:
lib/send-usdc.ts (EVM path)
transfer(to, amount) calldata. value must be BigInt(0) — no ETH is transferred.
Solana
Solana token transfers require Associated Token Accounts (ATAs) for both the sender and recipient. If the recipient’s ATA doesn’t exist, create it as part of the same transaction:
lib/send-usdc.ts (Solana path)
createTransferCheckedInstruction is used instead of createTransferInstruction because it includes the mint address and decimal count, making it safer against mint substitution attacks.