Skip to main content

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:
  1. Respond to RAMPS_CONFIG — send wallet address, chain, and API key when the widget loads
  2. Respond to RAMPS_CHECK_BALANCE — fetch the user’s on-chain USDC balance and return it
  3. Respond to RAMPS_SIGN_TRANSACTION — sign and broadcast the USDC transfer when the user confirms
Communication is entirely via 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
Validate all variables at startup so missing keys surface immediately:
lib/env.ts

Initialize Dynamic

Create the Dynamic client with EVM and Solana extensions. Both extensions must be registered immediately after createDynamicClient(). 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
In components that need auth state, import the hooks directly:
components/ramp-app.tsx

Configure chains

Define a MgChain 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
Always use chain: 'base' for Base transactions. Using chain: 'ethereum' for Base will cause a 502 error from the MoneyGram backend — they are treated as separate chains.

Authenticate and create wallets

Use the useWallet() 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’s readContract
  • Solana: resolves the Associated Token Account (ATA) for the user’s address and queries its token balance
lib/balance.ts
USDC uses 6 decimal places on both EVM and Solana. formatUnits(raw, 6) on EVM and uiAmountString on Solana both return human-readable values.

Handle the postMessage protocol

The widget communicates with your app via window.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
Why refs instead of state in event handlers?
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 fires RAMPS_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)
The transaction is sent to the USDC contract address with the 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.

Common mistakes

Additional resources

Last modified on June 24, 2026