Guide
Web integration · Solana / USDC
Core non-custodial Web flow: sessions, iframe SDK, Solana USDC signing, testing. Framework examples, API reference, and troubleshooting live on sibling pages.
MoneyGram Ramps — Web Integration Guide (Solana / USDC)
Version 1.0 · Solana + USDC · May 2026
MoneyGram Ramps lets partners embed cash-out (off-ramp) and cash-in (on-ramp) flows for USDC on Solana directly in web applications.
Cash-out (off-ramp): The user sends USDC on Solana. The widget handles KYC and quoting, calls your onSignTransaction callback to sign the transfer, then issues a MoneyGram reference number for cash pickup.
Cash-in (on-ramp): The user selects a MoneyGram location, deposits cash at the counter, and receives USDC in their Solana wallet. No on-chain signing is required in the happy path — the widget returns a confirmation code for the counter visit.
How the widget works: Your app loads the MoneyGram widget SDK in an iframe. The SDK handles session handoff, KYC, quoting, and the transaction UI. You provide wallet context and lifecycle callbacks.
Host: SDK setup, a raw iframe, and React Native share one message contract. Use Embed the widget. This guide covers the Solana non-custodial flow and the USDC transfer.
Transaction modes:
- New cash-out —
transaction.type: 'off-ramp'(default). Pre-fill destination country, amount, and KYC fields optionally. - New cash-in —
transaction.type: 'on-ramp'. User picks a deposit location inside the widget. - View mode — pass
viewTransaction.idto reopen an existing transaction (status, pickup instructions, refunds).
Supported asset: USDC on Solana only. Use the standard SPL USDC mint. Token 2022 / token-extensions USDC is not supported — see Step 5 for mint addresses.
Custodial partners: If you hold customer USDC in a shared omnibus wallet, use the Web · Custodial cash-out guide or React Native · Custodial cash-out guide. This guide covers non-custodial integrations only: end users sign transfers with their own wallet via onSignTransaction.
Why use the SDK? The SDK handles iframe management, postMessage communication, session management, and automatic resizing.
Integration time: 30–60 minutes for the complete setup
Chain: Solana
Asset: USDC
When onboarded you receive two credentials:
| Sandbox | Production | |
|---|---|---|
Public key (pk) | ramps_pk_sbox_... | ramps_pk_prod_... |
Secret key (sk) | ramps_sk_sbox_... | ramps_sk_prod_... |
The secret key must only ever be used server-side — never in client-side JavaScript.
Your domain must be allowlisted by MoneyGram before your keys work.
Quick reference
| Sandbox | Production | |
|---|---|---|
| Session API (server-side) | https://playground.xramps.moneygram.com/api/v1/sessions | Provided by MoneyGram |
| Widget URL | Returned by session API | Returned by session API |
| Public key prefix | ramps_pk_sbox_... | ramps_pk_prod_... |
| Secret key prefix | ramps_sk_sbox_... | ramps_sk_prod_... |
| USDC mint (mainnet) | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v | Same |
| USDC mint (devnet) | 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU | — |
| Real money | No | Yes |
| Reference numbers (cash-out) | Issued but not redeemable | Redeemable at MoneyGram locations |
| Confirmation codes (cash-in) | Issued for sandbox testing | Required at the counter |
| Cash-out (off-ramp) | Cash-in (on-ramp) | |
|---|---|---|
| User action | Send USDC on Solana | Deposit cash at a MoneyGram location |
| Widget mode | mode=off-ramp | mode=on-ramp |
| Location selection | Destination country (where to pick up cash) | MoneyGram store (where to deposit cash) |
| Partner callback | onSignTransaction required | Not used in the standard flow |
| Completion artifact | referenceNumber for cash pickup | Counter confirmation / reference for deposit |
| Crypto movement | User → MoneyGram deposit address | MoneyGram → user wallet (async after counter payment) |
Widget URL mode: The session widgetUrl already includes mode=off-ramp. For cash-in, replace it with searchParams.set('mode', 'on-ramp'). Do not append a second mode. Pass that URL as widgetUrl, and pass mode: 'on-ramp' to createRamps.
The secret key must never be exposed in client-side JavaScript. Create a server endpoint that calls MoneyGram's session API and returns only what the client needs.
The pattern below is plain Node.js — adapt the handler signature to your platform (Express, Next.js API routes, Cloudflare Worker, AWS Lambda, etc.).
// Generic Node.js handler — adapt signature to your platformasync function createMoneyGramSession(req, res) { // CORS — restrict to your own origins const origin = req.headers.origin const allowedOrigins = ['https://yourapp.com', 'http://localhost:3000'] if (allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin) res.setHeader('Vary', 'Origin') } res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS') res.setHeader('Access-Control-Allow-Headers', 'Content-Type') if (req.method === 'OPTIONS') return res.status(204).end() if (req.method !== 'POST') return res.status(405).end() const secretKey = process.env.MONEYGRAM_SK // ramps_sk_sbox_... — never in client JS if (!secretKey) return res.status(500).json({ error: 'Secret key not configured' }) const { walletAddress } = req.body ?? {} const mgiRes = await fetch('https://playground.xramps.moneygram.com/api/v1/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': secretKey }, body: JSON.stringify({ walletAddress, // end-user Solana address chain: 'solana', }), }) const data = await mgiRes.json() if (!mgiRes.ok) return res.status(mgiRes.status).json(data) return res.status(200).json({ sessionToken: data.sessionToken, // JWT — used by SDK sessionId: data.sessionId, widgetUrl: data.widgetUrl, // widget URL — provided by MoneyGram, do not hardcode })}
Response shape:
{ "sessionToken": "eyJhbGci...", "sessionId": "b4641a16-2b96-4afe-acb6-b71682305d08", "widgetUrl": "https://playground.xramps.moneygram.com/widget.html?mode=off-ramp"}
Agent ID: MoneyGram assigns an agent ID when they onboard you. It is automatically embedded in the sessionToken by their API — you never set or pass it yourself. The widget reads it from the token claims.
Session TTL: Session tokens expire after 1 hour. If the user leaves the widget open and returns later, the session will be expired and the widget will fail silently. Call your session endpoint fresh each time the user opens the widget — do not cache the session token.
Using CDN
Add the SDK via script tag:
<script src="https://playground.xramps.moneygram.com/sdk/index.global.js"></script><script> // SDK available as global const { createRamps } = window.RampsSDK</script>
Basic setup
With TypeScript:
// Type definitions (copy these into your project)type SupportedChain = 'solana' | 'stellar'type SupportedAsset = 'USDC'type WalletType = 'custodial' | 'non-custodial' interface OnChainTransaction { chain: SupportedChain to: string amount: string asset: SupportedAsset requiredNetwork?: 'mainnet' | 'testnet' tokenAddress?: string tokenDecimals?: number memo?: string rawTransaction: unknown} interface TransactionRecord { id: string type: 'off-ramp' | 'on-ramp' status: string chain: SupportedChain asset: SupportedAsset walletType: WalletType walletAddress: string amount: number referenceNumber?: string createdAt: string updatedAt: string} // Import SDK from global (loaded via <script> tag)declare const RampsSDK: { createRamps: (config: any) => any} const { createRamps } = RampsSDK // 1. Create container elementconst container = document.getElementById('ramps-widget-container') // 2. Fetch session from your backendconst session = await fetch('/api/moneygram-session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ walletAddress: userPublicKey }),}).then(res => res.json()) // 3. Initialize SDKconst ramps = createRamps({ container, sessionToken: session.sessionToken, // Wallet context wallet: { address: userPublicKey, // Solana public key chain: 'solana', asset: 'USDC', walletType: 'non-custodial', displayName: 'Your Wallet' // Optional - shown in widget header }, // Transaction signing callback onSignTransaction: async (tx: OnChainTransaction) => { console.log('Sign transaction:', tx) // Implement signing in Step 4 const signature = await signAndSendUsdc(tx) return signature }, // Lifecycle callbacks onReady: () => { console.log('Widget ready') }, onComplete: (transaction: TransactionRecord) => { console.log('Transaction complete:', transaction.referenceNumber) alert(`Cash pickup code: ${transaction.referenceNumber}`) }, onError: (error: { transactionId?: string; reason: string }) => { console.error('Widget error:', error) alert(`Error: ${error.reason}`) }, onClose: () => { console.log('Widget closed') }}) // 4. Open the widgetramps.open() // Optional: close programmatically// ramps.close() // Optional: cleanup (removes iframe)// ramps.destroy()
With pre-fill (optional)
Pre-fill customer info to reduce KYC friction:
const ramps = createRamps({ container, sessionToken: session.sessionToken, wallet: { /* ... */ }, // Customer pre-fill (all optional) customer: { firstName: 'John', lastName: 'Smith', email: '[email protected]', phone: '2145550100', phoneCountryDialCode: '+1', dateOfBirth: '1990-01-15' // YYYY-MM-DD }, // Transaction pre-fill (all optional) transaction: { type: 'off-ramp', destinationCountry: 'MEX', // ISO 3166-1 alpha-3 destinationCurrency: 'MXN', // ISO 4217 destinationSubdivision: 'MX-CMX', // ISO 3166-2 (required for MEX, USA, CAN) amount: 100, // Pre-fill amount asset: 'USDC' }, onSignTransaction: async (tx) => { /* ... */ }})
Customer field reference
When providing customer data, use these formats to ensure proper validation:
| Field | Format | Notes |
|---|---|---|
firstName, lastName | Non-empty string | Required if sending customer |
middleName, secondLastName | String | Optional |
dateOfBirth | YYYY-MM-DD | Regex-enforced by widget |
email | Valid email | Optional |
phone | National number, digits only | No country prefix, e.g. 2145550100 |
phoneCountryDialCode | Dial code with a leading +, e.g. +33 | Optional. Defaults to the pick-up country dial code when omitted. Send it when the customer phone country differs from the pick-up country |
addressLine1, city | Non-empty string | |
postalCode | String | Optional |
countryCode | ISO 3166-1 alpha-3 e.g. USA | Defaults to USA if omitted |
countrySubdivisionCode | ISO 3166-2 e.g. US-TX | Required for USA/MEX/CAN senders |
birthCountryCode, citizenshipCountryCode | ISO 3166-1 alpha-3 | Optional |
idType | PAS / DRV / STA / GOV | Passport / Driver's License / State ID / Gov ID |
idNumber | Non-empty string | |
idIssueCountry | ISO 3166-1 alpha-3 | |
idCountrySubdivisionCode | ISO 3166-2 e.g. US-TX | Required for DRV / STA issued in USA or Canada |
Note: The widget performs client-side validation on these fields. Ensure data matches the expected formats to avoid validation errors.
Note: phone and phoneCountryDialCode are independent of the pick-up country. A customer can hold a French number and collect cash in Lebanon. Send phone as the national number and phoneCountryDialCode as that number's own dial code. If the dial code is omitted, the widget falls back to the pick-up country dial code, which is wrong for any customer whose phone country differs.
Cash-in (on-ramp)
Open the widget in on-ramp mode. The user selects a MoneyGram location, completes KYC (including an on-ramp fraud warning), and receives a confirmation code to present at the counter. USDC is delivered to their wallet after MoneyGram processes the deposit.
const session = await fetch('/api/moneygram-session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ walletAddress: userPublicKey }),}).then(res => res.json()) // Replace mode on the widget URL from the session response. Do not append a second mode.const widgetUrl = new URL(session.widgetUrl)widgetUrl.searchParams.set('mode', 'on-ramp') const ramps = createRamps({ container, sessionToken: session.sessionToken, widgetUrl: widgetUrl.toString(), mode: 'on-ramp', wallet: { address: userPublicKey, chain: 'solana', asset: 'USDC', walletType: 'non-custodial', }, transaction: { type: 'on-ramp', amount: 100, // optional pre-fill (fiat amount) asset: 'USDC', }, // onSignTransaction is not called for standard cash-in flows onComplete: (transaction) => { console.log('Cash-in complete:', transaction.referenceNumber, transaction.type) }, onError: (error) => console.error(error), onClose: () => console.log('Widget closed'),}) ramps.open()
Cash-in flow inside the widget: select location → quote → KYC → fraud acknowledgment → review → commit → show counter instructions → poll until USDC arrives.
View existing transaction
Transaction ID vs. reference number: viewTransaction.id requires the Transaction ID (transaction.id from onComplete, e.g. tx_abc123), not the reference number / cash pickup code (transaction.referenceNumber, the 8-digit MoneyGram code the customer uses at the counter). The reference number identifies the transaction to MoneyGram and the customer; the Transaction ID identifies it to this API. Store both from onComplete, but pass only the Transaction ID to viewTransaction.
To show status of an existing transaction:
const ramps = createRamps({ container, sessionToken: session.sessionToken, viewTransaction: { id: 'tx_abc123' // Transaction ID (transaction.id from onComplete), not the reference number }, onClose: () => { console.log('User closed transaction view') }}) ramps.open()
The onSignTransaction callback is used for cash-out only. It receives a transaction payload when the user commits. Your app must sign and broadcast the USDC transfer, then return the transaction signature.
Network and token come from the API key
The widget derives sandbox vs production from your ramps_pk_* / ramps_sk_* key (_sbox_ → testnet, _prod_ → mainnet). You do not pass network or mint addresses separately.
Each onSignTransaction payload includes:
| Field | Description |
|---|---|
requiredNetwork | 'mainnet' or 'testnet' — match your wallet RPC/cluster to this |
tokenAddress | Canonical USDC mint/contract for that environment |
tokenDecimals | Token decimals (typically 6 for USDC) |
Always use tx.tokenAddress and tx.requiredNetwork from the payload instead of hardcoding devnet mints or RPC URLs. Sandbox keys expect Solana devnet. Production keys expect mainnet.
Balance check (optional early network gate)
Before Continue, the widget may send RAMPS_CHECK_BALANCE with requiredNetwork. Reply with RAMPS_BALANCE_RESULT as today. Optionally include blockchainNetwork ('mainnet' \| 'devnet' \| 'testnet') or evmChainId — when present, the widget blocks Continue if the wallet is on the wrong network. Omit these fields to preserve legacy behavior (fail open).
Prerequisites
Install Solana dependencies:
# Option A: Solana Kit (recommended for new projects)npm install @solana/kit @solana-program/token@^0.9.0 # Option B: Web3.js v1 (stable, widely used)npm install @solana/web3.js @solana/spl-token
@solana-program/token is the Kit-native successor to @solana/spl-token. Use it, not @solana/spl-token, when building on Kit.
These are standard Solana libraries available on npm, not MoneyGram-specific packages.
For wallet providers
This guide covers wallet applications integrating cash-out and cash-in. Use your existing wallet infrastructure to sign and broadcast USDC transfers for cash-out.
Key points:
- You already have the user's keypair and signing infrastructure
- Use your internal transaction signing methods (not external wallet adapters)
- The
onSignTransactioncallback receives transaction details from the widget - Return the transaction signature after broadcasting to Solana
Integration pattern:
onSignTransaction: async (tx: OnChainTransaction) => { // tx includes requiredNetwork, tokenAddress, tokenDecimals — derived from your API key. const rpcUrl = tx.requiredNetwork === 'mainnet' ? process.env.SOLANA_MAINNET_RPC! : process.env.SOLANA_DEVNET_RPC! const mint = new PublicKey(tx.tokenAddress!) // Use your wallet's internal signing method const signature = await yourWallet.signAndSendTransaction({ instructions: buildUsdcTransferInstructions(tx, mint), feePayer: userPublicKey, rpcUrl, }) return signature // Return the transaction signature string}
The examples below show how to construct the USDC transfer instructions using standard Solana libraries.
Option A: Solana Kit (Recommended) ⭐
Why Solana Kit:
- ✅ Modern functional API with pipe operators
- ✅ Better tree-shaking (smaller bundle size)
- ✅ Official future of Solana development
- ✅ Active development and support
When to use:
- New projects starting from scratch
- You prefer functional programming style
- Bundle size optimization is important
Solana Kit's plugin-based clients (@solana-program/token) condense the manual instruction-building shown in older guides into a single chained call:
import { address, createClient } from '@solana/kit'import { tokenProgram } from '@solana-program/token' // createClient() starts an empty client; .use(tokenProgram()) adds the// client.token namespace used below. Add your RPC plugin the same way// (see the Solana Kit docs for the current RPC plugin API), e.g.// createClient().use(solanaDevnetRpc).use(tokenProgram()).const client = createClient().use(tokenProgram()) async function signAndSendUsdc(tx: OnChainTransaction): Promise<string> { // Always derive the mint and decimals from the transaction, never // hardcode them. tx.tokenAddress/tokenDecimals reflect the network your // API key resolves to (devnet in sandbox, mainnet in production). if (!tx.tokenAddress || !tx.tokenDecimals) { throw new Error('Missing tokenAddress/tokenDecimals on transaction') } const mint = address(tx.tokenAddress) const decimals = tx.tokenDecimals // Signer wraps your wallet's signing method const signer = { address: address(yourWallet.getPublicKey()), signTransactions: async (transactions) => yourWallet.signTransactions(transactions), } const { context } = await client.token.instructions .transferToATA({ mint, authority: signer, recipient: address(tx.to), amount: toBaseUnits(tx.amount, decimals), decimals, }) .sendTransaction() return context.signature} // Use in SDKconst ramps = createRamps({ // ... onSignTransaction: signAndSendUsdc})
toBaseUnits(amount, decimals) converts a human-readable amount (e.g. "10.5") to the integer base-unit bigint USDC transfers expect. Use exact string parsing, not parseFloat, since floating-point math can lose precision or silently round amounts:
function toBaseUnits(amount: string, decimals: number): bigint { const [whole, fraction = ''] = amount.split('.') if (fraction.length > decimals) { throw new Error(`Amount has more than ${decimals} decimal places`) } const paddedFraction = fraction.padEnd(decimals, '0') return BigInt(whole + paddedFraction)}
Or use @solana-program/token's own helper if you already depend on it elsewhere.
Option B: Web3.js v1 (Stable / Legacy)
Why Web3.js v1:
- ✅ Battle-tested and stable
- ✅ Widely used (extensive community support)
- ✅ Familiar imperative API
- ✅ More Stack Overflow answers
When to use:
- Existing codebase already on web3.js v1
- Need maximum stability
- Prefer class-based API
import { Connection, PublicKey, Transaction } from '@solana/web3.js'import { getAssociatedTokenAddress, createTransferInstruction, TOKEN_PROGRAM_ID} from '@solana/spl-token' function toBaseUnits(amount: string, decimals: number): bigint { const [whole, fraction = ''] = amount.split('.') if (!/^\d+$/.test(whole) || (fraction && !/^\d+$/.test(fraction))) { throw new Error('Amount must be a decimal string') } if (fraction.length > decimals) { throw new Error(`Amount has more than ${decimals} decimal places`) } return BigInt(whole + fraction.padEnd(decimals, '0'))} async function signAndSendUsdc(tx: OnChainTransaction): Promise<string> { // Get wallet from your wallet provider const userPublicKey = yourWallet.getPublicKey() // Use your wallet's method if (!userPublicKey) { throw new Error('Wallet not connected') } if (!tx.tokenAddress || tx.tokenDecimals == null) { throw new Error('Missing tokenAddress or tokenDecimals on the sign payload') } if (tx.requiredNetwork !== 'mainnet' && tx.requiredNetwork !== 'testnet') { throw new Error('Sign payload is missing requiredNetwork') } const rpcUrl = tx.requiredNetwork === 'mainnet' ? process.env.SOLANA_MAINNET_RPC! : process.env.SOLANA_DEVNET_RPC! const connection = new Connection(rpcUrl, 'confirmed') // Parse transaction details from the payload. Do not hardcode a mint or use parseFloat. const toPublicKey = new PublicKey(tx.to) const amountBaseUnits = toBaseUnits(tx.amount, tx.tokenDecimals) const mintAddress = new PublicKey(tx.tokenAddress) // Get associated token accounts const fromAta = await getAssociatedTokenAddress( mintAddress, userPublicKey, false, TOKEN_PROGRAM_ID ) const toAta = await getAssociatedTokenAddress( mintAddress, toPublicKey, false, TOKEN_PROGRAM_ID ) console.log('Transfer:', { from: fromAta.toString(), to: toAta.toString(), amount: amountBaseUnits.toString() }) // Create transfer instruction const transferInstruction = createTransferInstruction( fromAta, toAta, userPublicKey, amountBaseUnits, [], TOKEN_PROGRAM_ID ) // Build transaction const transaction = new Transaction() transaction.add(transferInstruction) // Get recent blockhash const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash() transaction.recentBlockhash = blockhash transaction.feePayer = userPublicKey // Sign with your wallet const signedTransaction = await yourWallet.signTransaction(transaction) // Send transaction const signature = await connection.sendRawTransaction(signedTransaction.serialize()) console.log('Transaction sent:', signature) // Confirm transaction await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }) console.log('Transaction confirmed:', signature) return signature} // Use in SDKconst ramps = createRamps({ // ... onSignTransaction: signAndSendUsdc})
Comparison: Solana Kit vs Web3.js v1
| Feature | Solana Kit (v2) | Web3.js v1 |
|---|---|---|
| Status | Active development | Long-term maintenance |
| API style | Functional (pipe) | Class-based (OOP) |
| Bundle size | Smaller (tree-shaking) | Larger |
| Learning curve | Steeper (new patterns) | Gentler (familiar) |
| TypeScript | Excellent | Good |
| Community | Growing | Very large |
| Browser wallets | Supported | Supported |
| Production ready | Yes | Yes |
Recommendation:
- New projects: Start with Solana Kit for future-proofing
- Existing projects: Stay on web3.js v1 unless you have a reason to migrate
- Both work perfectly — choose based on your team's preferences
Handling errors
onSignTransaction: async (tx) => { try { // Validate user has enough balance const decimals = tx.tokenDecimals ?? 6 const required = toBaseUnits(tx.amount, decimals) const balance = await checkUsdcBalanceBaseUnits(tx.tokenAddress!) if (balance < required) { throw new Error(`Insufficient USDC balance. Need ${tx.amount} USDC`) } // Sign and send const signature = await signAndSendUsdc(tx) return signature } catch (error) { console.error('Transaction failed:', error) // User rejected signature if (error.message?.includes('rejected')) { throw new Error('Transaction cancelled by user') } // Network error if (error.message?.includes('fetch')) { throw new Error('Network error. Please try again.') } // Re-throw for widget to handle throw error }}
The widget will:
- Show the error message to the user
- Allow them to retry the transaction
- Call
onErrorcallback with details
Always use the official Circle USDC mint addresses:
| Network | Mint Address |
|---|---|
| Mainnet | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
| Devnet | 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU |
⚠️ Do not use Token-2022 USDC. The standard SPL token program is required.
Verify in your code:
const USDC_MINT_MAINNET = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'const USDC_MINT_DEVNET = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU' const usdcMint = process.env.NODE_ENV === 'production' ? USDC_MINT_MAINNET : USDC_MINT_DEVNET
After the core flow works:
- Add a framework example (React, Next.js, Vue, or Vanilla).
- Use the API reference for
createRamps, types, and callbacks. - Finish with the production checklist and troubleshooting.
For a shorter path, start with the Web non-custodial Quickstart.
Sandbox environment
The sandbox uses Solana devnet and issues real MoneyGram reference numbers (but they cannot be picked up at agent locations).
Cash-in (deposit) testing only works against a fixed list of sandbox agent locations. The widget highlights these locations in playground and other non-production environments. Search may show other nearby stores, but they are not set up for testing and will likely result in errors during the flow. Use one of the following:
| Country | Address | Location Name |
|---|---|---|
| USA | 2600 RICE CREEK RD, NEW BRIGHTON, MN 55112-5344 | CUB FOODS NEW BRIGHTON |
| USA | 1655 NEW YORK AVE, ARLINGTON, TX 76010-4766 | ELRODS COST PLUS - #7 - ARLINGTON |
| Canada | 407 LAURIER AVE W, OTTAWA, ON K1R 7Y7 | SPOT PLUS - CANADA POST |
| United Kingdom | 48 Elm Row, EDINBURGH, EH7 4AH | BM GLOBALX LIMITED - #UK161 |
| Ireland | 15 AUNGIER STREET, RATHMINES WEST, DUBLIN, CO DUBLIN D02 DD73 | WIRED - CASH AND DEBIT |
| Hungary | DOBO KORUT 8, KECSKEMET, BACS-KISKUN 6000 | CORNER TRADE KFT. |
| Poland | Al. Jerozlimskie 89 lok. 152 B C D, WARSZAWA, MAZOWIECKIE 02-001 | LITTLE INDIA SHOP - SKLEP LITTLE INDIA |
| Argentina | AVENIDA CORDOBA 947, RETIRO, BUENOS AIRES, CAPITAL FEDERAL 1054 | LATIN EXPRESS FINANCIAL SERVICES ARGENTI |
| Argentina | GENERAL MARIANO ACHA 4092, BUENOS AIRES, CAPITAL FEDERAL 1430 | SAAVEDRA COBRO EXPRESS - #LEAR1-1476 |
Test flow:
- Use devnet USDC:
4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU - Get devnet USDC from Solana faucet or Circle's testnet faucet
- Switch your wallet to devnet
- For cash-in, select one of the sandbox test locations above (shown at the top of the location picker in playground), not a real-world address
- Complete a transaction through the widget
- For cash-out you will receive a reference number (format: 8 digits); for cash-in you will receive a confirmation code
- Check transaction status via "View transaction" mode
Test scenarios
| Scenario | Expected behavior |
|---|---|
| Insufficient balance | Widget shows error before reaching signature step |
| User rejects signature | Widget allows retry |
| Network error | Widget shows error and allows retry |
| Successful cash-out | Widget shows reference number and completion screen |
| Successful cash-in | Widget shows counter confirmation code and "head to location" instructions |
| Cash-in without location | Continue button disabled until user selects a MoneyGram location |
| Session expired | Widget shows "Session expired, please refresh" |
Devnet USDC setup
// 1. Switch your wallet to devnet// (Implementation depends on your wallet) // 2. Get devnet SOL// Visit https://faucet.solana.com/ and enter your address // 3. Get devnet USDC// Visit https://faucet.circle.com/ and request USDC // 4. Verify balanceimport { Connection, PublicKey } from '@solana/web3.js' const connection = new Connection('https://api.devnet.solana.com')const mint = new PublicKey('4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU')const balance = await connection.getTokenAccountBalance(yourTokenAccount)console.log('USDC balance:', balance.value.uiAmount)