xmrswap

Swap XMR, SOL, USDT, and USDC, any chain

Pick a pair, enter an amount, and see what you'll get — XMR, SOL, USDT, or USDC on Solana, Ethereum, or Polygon. Then send to a one-time deposit address and track your order live.

You send
XMR
You get ≈
USDC
Service fee
Minimum

API

Base URL http://127.0.0.1:4100 · JSON in, JSON out · no auth on public endpoints · swap creation is rate-limited to 20/hour per IP.

POST/api/swaps

Create a swap. Returns a one-time Monero deposit subaddress and an indicative quote. Anything sent to that address (≥ the minimum, dust ignored) is swapped after 1 confirmation.

Request body

fieldtypedescription
to_chain"sol" | "eth" | "polygon"Chain to receive the payout on
to_token"usdc" | "usdt" | "sol"Optional payout token, default usdc; usdt and native sol are Solana only
to_addressstringDestination address: base58 for Solana, 0x-hex for EVM chains

Example

curl -X POST http://127.0.0.1:4100/api/swaps \
  -H 'Content-Type: application/json' \
  -d '{"to_chain":"sol","to_address":"Apewggxx…XbLp"}'

{
  "id": "984bcdce-4416-444b-bd28-28e8fd6d5af4",
  "status": "awaiting_deposit",
  "deposit_address": "88GHU2vA…5VA",
  "min_confirmations": 1,
  "min_deposit_xmr": 0.01,
  "expires_at": 1784655383973,
  "quote": { "usdc_per_xmr": 337.34, "source": "jupiter", "service_fee_bps": 100, "note": "float rate…" },
  "depeg_warning": false
}

Errors

statusmeaning
400to_chain not one of sol/eth/polygon, or the address fails validation for that chain
429rate limited (20 creations/hour per IP)
GET/api/swaps/:id

Swap status snapshot. Live updates are pushed over the WebSocket, so polling is not required (this UI falls back to 5-second polling only if the socket cannot connect). Amounts are decimal strings; XMR has 12 decimals, USDC 6.

Response fields

fielddescription
statussee lifecycle below
deposit_addressthe one-time XMR subaddress for this swap
deposited_xmrconfirmed XMR credited to the swap
unconfirmed_xmrXMR seen in the mempool, not yet 1-conf
delivered_usdcnet USDC sent, once completed
delivery_refSolana tx signature, or the Mayan order signature for eth/polygon (track at explorer.mayan.finance/swap/<sig>)
delivery_fulfill_refeth/polygon only: the destination-chain tx hash of the observed Mayan fill, set when the swap completes
expires_atepoch ms; deposits after expiry are held for manual review, never lost
GET/api/swaps/:id/qr

The deposit address as a QR code (SVG, monero: URI). Suitable for direct use in an <img>.

GET/api/quote

Current indicative rate and limits. The executable rate comes from Jupiter (selling wXMR→USDC on Solana: real liquidity, not an oracle); CoinGecko XMR/USD is returned as a reference. depeg_warning is set when they diverge >5%. max_xmr is the live wXMR reserve balance — the most a forward swap can pay out right now. Add ?amount_xmr= to price a specific size: the response gains sized with the effective rate at that amount, price_impact_pct, drift_bps vs the ambient quote, and would_park — true when the executor's drift guard would hold that size for review, i.e. the pool cannot absorb it. Add ?out=usdt|sol to quote a USDT or native-SOL payout instead; usdc_per_xmr and the sized fields are then denominated in that token (see out_token).

Example

curl http://127.0.0.1:4100/api/quote

{
  "usdc_per_xmr": 337.34,
  "xmr_usd_reference": 336.5,
  "source": "jupiter",
  "depeg_warning": false,
  "service_fee_bps": 100,
  "min_deposit_xmr": 0.01,
  "min_confirmations": 1,
  "chains": ["sol", "eth", "polygon"]
}
POST/api/reverse-swaps

Create a reverse swap: USDC on Solana in, XMR out. Returns a one-time Solana deposit address; send USDC there directly, or from any Mayan-supported chain by setting it as the recipient of a Mayan swap to USDC on Solana. Once at least min_usdc_in is finalized at the address, the service buys wXMR and redeems it through the wxmr.io bridge straight to your Monero address. Amounts are always read from the chain; nothing in the request body can influence them.

Request body

fieldtypedescription
xmr_addressstringMonero mainnet payout address (standard or subaddress; integrated addresses rejected)
from_token"usdc" | "usdt" | "sol"Optional deposit token on Solana, default usdc; the response's deposit_token names exactly what the address is watched for

Example

curl -X POST http://127.0.0.1:4100/api/reverse-swaps \
  -H 'Content-Type: application/json' \
  -d '{"xmr_address":"44AFFq5kSiGBoZ…Wjw"}'

{
  "id": "7c2f1de0-9b31-47a5-a1c4-52ab61a1b2c3",
  "direction": "usdc_to_xmr",
  "status": "awaiting_payment",
  "deposit_address": "9x1FZk…Qm2",
  "deposit_chain": "sol",
  "deposit_token": "USDC",
  "min_usdc_in": "36.70",
  "expires_at": 1784655383973,
  "quote": { "usdc_per_xmr": 351.02, "source": "jupiter", "service_fee_bps": 100, "note": "float rate…" },
  "depeg_warning": false
}

Errors

statusmeaning
400xmr_address fails wallet-side validation (wrong network, integrated, malformed)
429rate limited (20 creations/hour per IP, separate budget from /api/swaps)
GET/api/reverse-swaps/:id

Reverse swap status snapshot; live updates on the WebSocket reverse_swap channel.

Response fields

fielddescription
statussee reverse lifecycle below
deposit_addressthe one-time Solana address to pay USDC to
min_usdc_inUSDC required to credit; covers the bridge's 0.1 XMR withdrawal minimum at the quoted rate
usdc_receivedUSDC observed (finalized) at the deposit address
delivered_xmrnet XMR the bridge sends, once known
xmr_txid, xmr_tx_keythe payout transaction and its tx key: verifiable proof of payment to your address
expires_atepoch ms; payments after expiry are held for manual review, never lost
GET/api/reverse-swaps/:id/qr

The deposit address as a QR code (SVG, solana: Pay URI carrying the USDC mint).

GET/api/reverse-quote

Indicative rate for the reverse direction, quoted on the buy side (what USDC actually buys of wXMR on Jupiter), plus min_usdc_in. bridge_fee_bps is the wxmr.io bridge's withdrawal fee, read live from its on-chain config; it is taken out of the withdrawn amount before the XMR payout. Add ?amount_usdc= for a sized quote with the same sized shape as /api/quote; there is no reserve cap in this direction, so would_park is the effective liquidity ceiling. Add ?in=usdt|sol to quote a USDT or native-SOL deposit instead; rates, minimums, and sized amounts are then denominated in that token (see in_token).

Reverse swap lifecycle
awaiting_paymentdeposit address issued, nothing finalized yet
payment_received≥ minimum USDC finalized, queued for execution
executingsweeping the deposit and buying wXMR at the live rate
withdrawingbridge withdrawal requested; XMR payout in flight
completeddone; xmr_txid and xmr_tx_key are final
expiredno payment within 24 h; late payments are held for review
under_reviewparked for a human; funds are never silently dropped
GET/api/routes

Directed pairs the routing core serves: every pair across usdc-sol, usdc-eth, usdc-poly, sol (native SOL), and usdt-sol (USDT on Solana) — twenty in total, no XMR leg. Each routed swap is a chain of legs where every leg's input is the chain-observed output of the previous leg; pairs involving SOL or USDT add one Jupiter conversion leg through the USDC-on-Solana hub. EVM deposits are credited from finalized Transfer logs of the chain's native USDC contract only (named in the response), swept gaslessly via EIP-3009, and paid out of Solana reserves.

GET/api/route-quote?from=usdc-sol&to=usdc-eth

Composed quote for a route, priced at min_in — the worst case the loss budget permits, since the bridge's relayer fees are roughly fixed per order and weigh heaviest on small amounts; larger deposits only do better. Execution re-prices at the actual received amount and parks anything quoted to lose more than max_loss_bps. Add &amount= (in units of the from-asset; amount_usdc is an accepted alias) to price the route at that size instead of the floor. Amounts in and out are denominated in from_token / to_token. max_usdc is the live Solana USDC reserve balance; it caps EVM-in routes, whose deliveries are fronted from reserves, and is null for Solana deposits, which convert the user's own swept funds.

POST/api/routed-swaps

Create a routed swap. Body: {"from":"usdc-eth","to":"usdc-sol","to_address":"…"} (any pair from GET /api/routes). Returns a one-time deposit address on the from-chain (also as a QR at /api/routed-swaps/:id/qr) plus min_in, with deposit_token naming exactly what the address is watched for (USDC, USDT, or native SOL). EVM deposits pay a network_fee_usdc deducted from the output — the sweep transaction's actual gas cost (estimated in the quote, charged from the receipt). For EVM deposits send native USDC on the named chain only: the same 0x address exists on every chain, so wrong-chain or wrong-token funds are held for manual recovery, never credited or lost. Send at least min_in of the deposit token; the response's legs array reports each leg's status and chain-observed output as the swap runs. Statuses: awaiting_deposit → executing → delivering → completed with the same expired / under_review exits and never-drop-money rules as both other flows. delivered_usdc is the amount observed delivered on the destination chain, delivery_ref the Mayan order signature, delivery_fulfill_ref the destination-chain tx. Snapshot at GET /api/routed-swaps/:id; live pushes on the routed_swap WebSocket channel.

GET/api/health

Service health: Monero wallet and daemon heights, sync state, and whether execution is dry-run or live. Returns 503 when the wallet RPC is unreachable.

WS/api/wsWebSocket upgrade

Push channel so nothing has to poll. Send JSON subscribe / unsubscribe ops; every subscription is answered with a snapshot immediately, then swap, reverse_swap, and routed_swap push on every state change while quote and health push a fresh snapshot every 30 s. Payload shapes match the REST endpoints above.

Example

// wss://<host>/api/ws (lines starting with > are sent, < received)
> {"op":"subscribe", "channel":"swap", "id":"984bcdce-4416-444b-bd28-28e8fd6d5af4"}
< {"channel":"swap", "swap":{"id":"984bcdce-…", "status":"awaiting_deposit", …}}
< {"channel":"swap", "swap":{"id":"984bcdce-…", "status":"deposit_unconfirmed", …}}
> {"op":"subscribe", "channel":"quote"}
< {"channel":"quote", "quote":{"usdc_per_xmr": 337.34, …}}
Swap lifecycle
awaiting_depositsubaddress issued, nothing received yet
deposit_unconfirmeddeposit seen in the mempool
deposit_confirmed1 confirmation reached, queued for execution
executingselling wXMR reserves → USDC at the live rate
deliveringUSDC payout in flight; for eth/polygon the swap stays here until the Mayan order's fill is observed on the destination chain
completeddone; delivered_usdc is the observed delivered amount and delivery_ref is final
expiredno deposit within 24 h; late deposits are held for review
under_reviewparked for a human (late/extra deposit, price moved >3% against the quote, or an execution error); funds are never silently dropped