npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@meldcrypto/sdk

v0.2.0

Published

Meld SDK — embed crypto on/off-ramp provider widgets (Mercuryo card, …) into your checkout.

Readme

Meld SDK

Embed crypto on/off-ramp provider widgets into your own checkout with one uniform call: Meld.mount(order, hostElement, handlers). Create the order on your backend (your Meld API key never reaches the browser), pass the response to Meld.mount on your frontend, and the SDK loads the right provider widget, mounts its cross-origin iframe into a container you own, and relays lifecycle events back to you.

The SDK is a container manager and event relay: it never renders card input, never reads or transports PAN/CVC, and never reaches into the provider's iframe — card capture happens entirely on the provider's PCI surface.

Which adapter runs is decided internally from the order's (paymentMethodType, renderMode), so adding a provider is never a public-API change. Supported today: Mercuryo — card.

Installation

npm install @meldcrypto/sdk
import Meld from '@meldcrypto/sdk';

Meld.configure({ environment: 'sandbox' }); // or 'production'

Use it with a bundler (Webpack / Vite / etc.) — the SDK is bundled into your app. If your page sets a CSP, add the provider hosts from requiredCsp to your directives.

Integration

Two calls, the same for every provider: your backend creates the order, and your frontend passes the response verbatim to Meld.mount.

1 · Backend — create the order

POST {base}/crypto/order/headless with Authorization: BASIC <your-api-key> and a Meld-Version header. Base URL: https://api-sb.meld.io (sandbox) / https://api.meld.io (production). The request's provider/payment-method fields are provider-specific — see Providers. Return the response to your frontend untouched; the SDK reads what it needs from it:

{
  "id": "…",
  "paymentMethodType": "…",
  "paymentMethodResponseDetails": {
    "serviceProviderWidgetUrl": "…",
    "renderMode": "…"
  }
}

paymentMethodResponseDetails carries whatever the chosen provider's adapter needs (a signed widget URL, a session token, …) — fields vary by provider. Don't read or reshape it; pass the whole order through and the SDK picks out what it needs.

2 · Frontend — mount the order

const order = await fetch('/your-backend/order').then(r => r.json()); // step 1's response

if (!Meld.capabilities(order).embeddable) {
  // not an embeddable order for this SDK — handle it outside the SDK
}

const handle = Meld.mount(order, document.getElementById('widget-container'), {
  onReady:            (e) => hideSpinner(),    // e = { orderId }
  onPaymentSubmitted: (e) => showProcessing(), // ⚠ UX hint — settlement is YOUR webhook, not this
  onStatusChange:     (s) => { if (s.status === 'completed') showOrderComplete(); },
  onCancel:           (e) => showRetryCta(),   // e = { orderId }
  onError:            (e) => showError(e.message), // e = { orderId, code, message, detail?, recoverable }
});

3 · Tear down

handle.unmount();   // removes the iframe; call on navigation / modal close

Loading & lifecycle

  • Between mount() and onReady the widget is loading (provider script + iframe first paint) — typically a second or two, but onReady is synthesized with fallback timers, so don't assume it's instant. Show a spinner/skeleton on mount() and clear it on onReady or onError (an error can arrive instead of ready).
  • Pair mount with unmount. Call handle.unmount() when the widget goes away (navigation, modal close, component unmount). In React, call it in the effect cleanup — this also makes Strict Mode's mount→unmount→mount cycle safe.
  • Mounting into a container that already holds a widget clears it first (one widget per container), so a stray double-mount() won't stack two iframes — but pairing with unmount is still the correct pattern.

Events

| Event | Fires when | Do | |---|---|---| | onReady | Widget mounted & interactive (native or synthesized) | Hide spinner | | onPaymentSubmitted | User finished/submitted the provider payment flow (UX hint only) | Show "processing" | | onStatusChange | Provider order status changed; payload { orderId, status, providerStatus, raw } where status is pending | completed | failed | cancelled | React to status; completed = provider "order complete" (still not settlement) | | onCancel | Provider reported a cancelled status | Retry CTA | | onError | Script load failure or terminal failed status | Show error; recoverable says retry vs. new order |

status is normalized across providers — you code against completed/failed/etc., never a provider's raw status string (that's available in providerStatus for logging). A terminal failed also fires onError, and cancelled also fires onCancel, so handlers that only listen for those still work.

Every callback carries the order it relates to — onReady/onPaymentSubmitted/onCancel receive { orderId }, and onStatusChange/onError carry it on their payload — so an app driving several orders at once can tell them apart.

Which events a provider emits varies: onStatusChange fires only for providers that surface interim/terminal status (Mercuryo does), while onPaymentSubmitted/onCancel/onError are the common lifecycle signals. Whatever the client events say, settlement is always the Meld webhook (below) — so a robust integration works off onError/onCancel plus the webhook, treating the rest as UX hints.

To tear the widget down yourself (navigation, modal close, user backs out), call handle.unmount() — see Tear down. That removes the iframe but does not emit onCancel; the lifecycle events fire only from provider-driven transitions.

Settlement — webhook, never the SDK

Neither onPaymentSubmitted nor onStatusChange with status: 'completed' is settlement — both are client-side UX signals from the provider widget. Mark the order paid only when your backend receives Meld's TRANSACTION_STATUS_CHANGED webhook. Show "processing", not "success", until then. (The event is named onPaymentSubmitted, not onComplete, for exactly this reason — submitted ≠ settled.)

Content-Security-Policy

If your page sets a CSP, ask the SDK what each provider needs rather than hard-coding hosts or permissions. requiredCsp takes any list of providers and returns the merged, de-duped directives for the environment set in Meld.configure — everything is read from the SDK's provider registry, so there's nothing provider-specific for you to maintain:

const csp = Meld.requiredCsp(['MERCURYO']); // example — the result for Mercuryo, sandbox
// {
//   frameSrc:    ['https://sandbox-widget.mrcr.io', 'https://sandbox-exchange.mrcr.io'],
//   scriptSrc:   ['https://sandbox-widget.mrcr.io/embed.2.1.js'],
//   iframeAllow: ['camera']
// }

Map the result onto your CSP header and the iframe's allow attribute:

| Field | Use | |---|---| | frameSrc | append to your frame-src directive | | scriptSrc | append to your script-src directive | | iframeAllow | the container iframe's allow attribute |

Hosts and permissions differ between sandbox and production; calling requiredCsp after Meld.configure({ environment }) returns the correct set, so you don't branch on environment yourself.

The environment of a signed order is authoritative — if you mount an order whose environment differs from the one passed to Meld.configure (e.g. a production order while still configured for sandbox), the SDK logs a console.warn, since your CSP was built from requiredCsp for the other environment and would block the widget. Call Meld.configure({ environment }) to match the order before generating your CSP.

API reference

  • Meld.configure(options){ environment: 'sandbox' | 'production' }.
  • Meld.mount(order, hostElement, handlers)handle — mounts the provider widget; handle.unmount() removes it.
  • Meld.capabilities(order){ embeddable, surface, requiresUserGesture } — guard with embeddable before calling mount.
  • Meld.requiredCsp(providers){ frameSrc, scriptSrc, iframeAllow } — CSP directives for the enabled providers.

Providers

The core integration above is identical for every provider. This section holds the provider-specific pieces: which fields your backend sends, and any provider onboarding. Each supported provider is its own subsection; new providers appear here without changing the core API or your mount call.

Mercuryo — card

Embedded iframe (renderMode: IFRAME). The SDK loads Mercuryo's widget script and mounts it from the signed URL in the order — you don't touch those internals.

Pre-flight — do these first, or a correct integration still fails:

  1. Allowlist your embedding origin on the Mercuryo widget (exact, HTTPS origin). If it's not allowlisted the iframe is silently refused — the most opaque failure here, since it looks like a code bug. (Sandbox is also IP-allowlisted.)
  2. The customer must have APPROVED Sumsub KYC linked to their Meld customer before the order — otherwise order creation fails with KYC_NOT_COMPLETED. (Details below.)
  3. Headless onramps enabled for your Meld account.

These are onboarding/account steps, not code — but they surface as code-looking failures mid-integration, so confirm them up front.

Create the order (POST /crypto/order/headless):

curl -X POST https://api-sb.meld.io/crypto/order/headless \
  -H "Authorization: BASIC $MELD_API_KEY" \
  -H "Meld-Version: 2026-05-01" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId":               "<meld-customer-id>",
    "externalOrderId":          "<your-unique-order-id>",
    "sessionType":              "BUY",
    "serviceProvider":          "MERCURYO",
    "paymentMethodType":        "CREDIT_DEBIT_CARD",
    "sourceCurrencyCode":       "USD",
    "sourceAmount":             "15",
    "destinationCurrencyCode":  "BTC",
    "destinationWalletAddress": "<wallet-address>",
    "countryCode":              "US",
    "clientIpAddress":          "<end-user-public-ip>"
  }'
  • No redirectUrl — Mercuryo card orders are iframe-mode and reject one with INVALID_REDIRECT_URL.
  • Pass the end user's public IP as clientIpAddress. Mercuryo binds the widget signature to it and recomputes it from the IP the user's browser presents to the widget host. A mismatch (your backend's IP via X-Forwarded-For, or an IPv4/IPv6 family flip) surfaces in the widget as "wallet address invalid".

Quote (optional, to price your checkout UI): POST /payments/crypto/quote with the same corridor fields. Pass ?integrationMode=HEADLESS so only headless-capable providers are returned.

Onboarding & prerequisites:

  • KYC (out-of-band, beforehand): the customer needs a completed Sumsub verification (status APPROVED) linked to their Meld customer — done either via Meld's Unified KYC or your own KYC flow with the Sumsub token shared to Meld. POST /crypto/order/headless doesn't run or wait on KYC; because KYC-token sharing is enabled between Meld and Mercuryo, Meld mints a fresh Mercuryo-scoped share token on the fly at order creation and embeds it in the signed widget URL, so the widget skips its own KYC. No APPROVED verification → creation fails with KYC_NOT_COMPLETED.
  • Origin allowlist: your exact embedding origin must be registered on the Mercuryo widget, and only HTTPS origins are honoredhttp:// origins (including localhost) never make it into Mercuryo's frame-ancestors header, so the frame is refused. Sandbox is additionally IP-allowlisted; a 403 on the widget script means the allowlist, not the SDK. This is an onboarding step, not a code change.
  • Camera: Mercuryo's in-iframe KYC liveness needs camera in the iframe allow attribute — surfaced via requiredCsp().iframeAllow.