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.

Core flowSolanaUSDC

MoneyGram Ramps — Web Integration Guide (Solana / USDC)

Version 1.0 · Solana + USDC · May 2026

Overview

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.id to 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:

SandboxProduction
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

SandboxProduction
Session API (server-side)https://playground.xramps.moneygram.com/api/v1/sessionsProvided by MoneyGram
Widget URLReturned by session APIReturned by session API
Public key prefixramps_pk_sbox_...ramps_pk_prod_...
Secret key prefixramps_sk_sbox_...ramps_sk_prod_...
USDC mint (mainnet)EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1vSame
USDC mint (devnet)4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU—
Real moneyNoYes
Reference numbers (cash-out)Issued but not redeemableRedeemable at MoneyGram locations
Confirmation codes (cash-in)Issued for sandbox testingRequired at the counter

Cash-out (off-ramp)Cash-in (on-ramp)
User actionSend USDC on SolanaDeposit cash at a MoneyGram location
Widget modemode=off-rampmode=on-ramp
Location selectionDestination country (where to pick up cash)MoneyGram store (where to deposit cash)
Partner callbackonSignTransaction requiredNot used in the standard flow
Completion artifactreferenceNumber for cash pickupCounter confirmation / reference for deposit
Crypto movementUser → MoneyGram deposit addressMoneyGram → 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.).

TypeScript
// 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:

JSON
{  "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:

HTML
<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:

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:

TypeScript
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:

FieldFormatNotes
firstName, lastNameNon-empty stringRequired if sending customer
middleName, secondLastNameStringOptional
dateOfBirthYYYY-MM-DDRegex-enforced by widget
emailValid emailOptional
phoneNational number, digits onlyNo country prefix, e.g. 2145550100
phoneCountryDialCodeDial code with a leading +, e.g. +33Optional. Defaults to the pick-up country dial code when omitted. Send it when the customer phone country differs from the pick-up country
addressLine1, cityNon-empty string
postalCodeStringOptional
countryCodeISO 3166-1 alpha-3 e.g. USADefaults to USA if omitted
countrySubdivisionCodeISO 3166-2 e.g. US-TXRequired for USA/MEX/CAN senders
birthCountryCode, citizenshipCountryCodeISO 3166-1 alpha-3Optional
idTypePAS / DRV / STA / GOVPassport / Driver's License / State ID / Gov ID
idNumberNon-empty string
idIssueCountryISO 3166-1 alpha-3
idCountrySubdivisionCodeISO 3166-2 e.g. US-TXRequired 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.

TypeScript
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:

TypeScript
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:

FieldDescription
requiredNetwork'mainnet' or 'testnet' — match your wallet RPC/cluster to this
tokenAddressCanonical USDC mint/contract for that environment
tokenDecimalsToken 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:

Bash
# 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 onSignTransaction callback receives transaction details from the widget
  • Return the transaction signature after broadcasting to Solana

Integration pattern:

TypeScript
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.


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:

TypeScript
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:

TypeScript
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
TypeScript
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

FeatureSolana Kit (v2)Web3.js v1
StatusActive developmentLong-term maintenance
API styleFunctional (pipe)Class-based (OOP)
Bundle sizeSmaller (tree-shaking)Larger
Learning curveSteeper (new patterns)Gentler (familiar)
TypeScriptExcellentGood
CommunityGrowingVery large
Browser walletsSupportedSupported
Production readyYesYes

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

TypeScript
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:

  1. Show the error message to the user
  2. Allow them to retry the transaction
  3. Call onError callback with details

Always use the official Circle USDC mint addresses:

NetworkMint Address
MainnetEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
Devnet4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU

⚠️ Do not use Token-2022 USDC. The standard SPL token program is required.

Verify in your code:

TypeScript
const USDC_MINT_MAINNET = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'const USDC_MINT_DEVNET = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU' const usdcMint = process.env.NODE_ENV === 'production'   ? USDC_MINT_MAINNET   : USDC_MINT_DEVNET


Next steps

After the core flow works:

  1. Add a framework example (React, Next.js, Vue, or Vanilla).
  2. Use the API reference for createRamps, types, and callbacks.
  3. Finish with the production checklist and troubleshooting.

For a shorter path, start with the Web non-custodial Quickstart.

Step 7 — Testing

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:

CountryAddressLocation Name
USA2600 RICE CREEK RD, NEW BRIGHTON, MN 55112-5344CUB FOODS NEW BRIGHTON
USA1655 NEW YORK AVE, ARLINGTON, TX 76010-4766ELRODS COST PLUS - #7 - ARLINGTON
Canada407 LAURIER AVE W, OTTAWA, ON K1R 7Y7SPOT PLUS - CANADA POST
United Kingdom48 Elm Row, EDINBURGH, EH7 4AHBM GLOBALX LIMITED - #UK161
Ireland15 AUNGIER STREET, RATHMINES WEST, DUBLIN, CO DUBLIN D02 DD73WIRED - CASH AND DEBIT
HungaryDOBO KORUT 8, KECSKEMET, BACS-KISKUN 6000CORNER TRADE KFT.
PolandAl. Jerozlimskie 89 lok. 152 B C D, WARSZAWA, MAZOWIECKIE 02-001LITTLE INDIA SHOP - SKLEP LITTLE INDIA
ArgentinaAVENIDA CORDOBA 947, RETIRO, BUENOS AIRES, CAPITAL FEDERAL 1054LATIN EXPRESS FINANCIAL SERVICES ARGENTI
ArgentinaGENERAL MARIANO ACHA 4092, BUENOS AIRES, CAPITAL FEDERAL 1430SAAVEDRA COBRO EXPRESS - #LEAR1-1476

Test flow:

  1. Use devnet USDC: 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU
  2. Get devnet USDC from Solana faucet or Circle's testnet faucet
  3. Switch your wallet to devnet
  4. 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
  5. Complete a transaction through the widget
  6. For cash-out you will receive a reference number (format: 8 digits); for cash-in you will receive a confirmation code
  7. Check transaction status via "View transaction" mode

Test scenarios

ScenarioExpected behavior
Insufficient balanceWidget shows error before reaching signature step
User rejects signatureWidget allows retry
Network errorWidget shows error and allows retry
Successful cash-outWidget shows reference number and completion screen
Successful cash-inWidget shows counter confirmation code and "head to location" instructions
Cash-in without locationContinue button disabled until user selects a MoneyGram location
Session expiredWidget shows "Session expired, please refresh"

Devnet USDC setup

TypeScript
// 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)