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

naira-ramp

v0.8.0

Published

Unified crypto on-ramp and off-ramp SDK for Nigeria. Abstracts YellowCard and Quidax into a single API with React hooks, widgets, multi-chain support (Ethereum, Tron, Base, Optimism, BSC), and webhook handling.

Readme

naira-ramp

Unified crypto on-ramp and off-ramp SDK for Nigeria. Abstracts YellowCard and Quidax into a single API with React hooks and drop-in widgets.

Features

  • Single NairaRamp class for on-ramp and off-ramp
  • Supports USDT and USDC
  • Auto-selects the best provider rate
  • In-memory rate cache with configurable TTL
  • Automatic retry with exponential backoff on network and server errors
  • Transaction history with pluggable store (MemoryStore, LocalStorageStore, or custom)
  • Webhook support — receive push status updates server-side with signature verification
  • React hooks (useRates, useOfframp, useOnramp) — works on web and React Native
  • Drop-in React widgets (OfframpWidget, OnrampWidget) — inline and modal variants, theme:'auto', close button/Escape dismiss
  • Sandbox mode for development without real API keys

How It Works

┌─────────────────────────────────────────────────────────────────────┐
│                        Your Application                             │
│                                                                     │
│   React Web          React Native         Node.js Server            │
│  ┌──────────┐        ┌──────────┐        ┌──────────────────┐       │
│  │ Widget   │        │  Hooks   │        │  handleWebhook() │       │
│  │Offramp / │        │useRates  │        │  (verify + parse │       │
│  │ Onramp  │        │useOfframp│        │   + fire callbacks│       │
│  └────┬─────┘        │useOnramp │        └────────┬─────────┘       │
│       │              └────┬─────┘                 │                 │
└───────┼───────────────────┼───────────────────────┼─────────────────┘
        │                   │                       │ webhook POST
        ▼                   ▼                       ▼
┌─────────────────────────────────────────────────────────────────────┐
│                         NairaRamp                                   │
│                                                                     │
│  getRate()  ──►  Router (selectProvider)  ──►  Rate Cache (TTL)     │
│                        │                                            │
│                  ┌─────┴──────┐                                     │
│                  │ auto mode? │                                     │
│                  └─────┬──────┘                                     │
│            No ◄────────┴────────► Yes                               │
│       (use preferred)      (allSettled → best rate)                 │
│                                                                     │
│  offramp()  ──►  selectProvider  ──►  initiateOfframp()             │
│                                            │                        │
│  onramp()   ──►  selectProvider  ──►  initiateOnramp()              │
│                                            │                        │
│                                     TransactionStore                │
│                                     (save + update)                 │
│                                            │                        │
│  getTransaction()  ──────────────►  poll status  ──►  callbacks     │
│                                                                     │
│  Retry: NETWORK_ERROR / 5xx  →  exponential backoff (×3 default)    │
└──────────────────────┬──────────────────────────────────────────────┘
                       │
          ┌────────────┴────────────┐
          ▼                         ▼
┌─────────────────┐       ┌──────────────────┐
│   YellowCard    │       │     Quidax       │
│                 │       │                  │
│ HMAC-SHA256 auth│       │ Bearer token auth│
│ GET  /rates     │       │ GET  /tickers    │
│ POST /payments  │       │ POST /instant_   │
│ GET  /payments/ │       │      orders      │
│      :id        │       │ GET  /instant_   │
│ GET  /account-  │       │      orders/:id  │
│      lookup     │       │                  │
│                 │       │ verifyBankAccount│
│ Sandbox: ✅     │       │ → returns input  │
│ (sandbox URL)   │       │   unchanged      │
└─────────────────┘       └──────────────────┘

Off-ramp flow (Crypto → NGN)

User enters amount + bank details
        │
        ▼
verifyBankAccount() ──► YellowCard: real lookup
                    ──► Quidax: returns input as-is
        │
        ▼
getRate() ──► Router picks best NGN/USDT rate
        │
        ▼
offramp() ──► initiateOfframp() ──► Transaction created
        │                                │
        │                         store.save(tx)
        │                         callbacks registered
        ▼
Poll / Webhook ──► status: pending → processing → completed
        │                                              │
        ▼                                              ▼
onStatusChange()                              onSuccess(tx)

On-ramp flow (NGN → Crypto)

User enters NGN amount + wallet address
        │
        ▼
getRate() ──► Router picks lowest rate
        │
        ▼
onramp() ──► initiateOnramp() ──► Transaction created
        │                               │
        │                         store.save(tx)
        ▼
Poll / Webhook ──► status: pending → processing → completed
                                                       │
                                                onSuccess(tx)
                                          crypto sent to wallet

Installation

npm install naira-ramp

React is a peer dependency (optional — only needed for hooks and widgets):

npm install react

Quick Start

import { NairaRamp } from 'naira-ramp';

const ramp = new NairaRamp({
  yellowcardApiKey: process.env.YELLOWCARD_API_KEY,
  yellowcardSecretKey: process.env.YELLOWCARD_SECRET_KEY,
  quidaxApiKey: process.env.QUIDAX_API_KEY,
  defaultProvider: 'auto', // picks best rate automatically
});

// Get current rate
const rate = await ramp.getRate({ currency: 'USDT', type: 'offramp' });
console.log(`1 USDT = ₦${rate.rate}`);

// Sell crypto (off-ramp)
const tx = await ramp.offramp({
  amount: 100, // USDT
  currency: 'USDT',
  bankAccount: {
    accountNumber: '0123456789',
    bankCode: '058',
  },
});

Configuration

const ramp = new NairaRamp({
  // YellowCard credentials (both required to enable YellowCard)
  yellowcardApiKey: '...',
  yellowcardSecretKey: '...',

  // Quidax credentials
  quidaxApiKey: '...',

  // Provider selection: 'yellowcard' | 'quidax' | 'auto' (default: 'auto')
  defaultProvider: 'auto',

  // Rate cache TTL in ms (default: 30000)
  rateCacheTtlMs: 30_000,

  // Enable sandbox/mock mode (default: false)
  sandbox: false,

  // Transaction persistence (default: MemoryStore — in-memory, lost on refresh)
  store: new LocalStorageStore(), // persist across sessions

  // Retry configuration
  maxRetries: 3, // max attempts after first failure (default: 3)
  retryDelayMs: 500, // initial delay before first retry in ms (default: 500)
  retryBackoffMultiplier: 2, // multiplier applied to delay on each retry (default: 2)

  // Merchant rate spread (Quidax only, default: 0 — no adjustment)
  // merchantPercent: 0.5, // adds 0.5% to onramp rate, subtracts 0.5% from offramp rate
});

Retry Behaviour

The SDK automatically retries failed requests using exponential backoff. Retries are applied to getRate(), offramp(), onramp(), and getTransaction().

What gets retried:

| Error | Retried | Reason | | ------------------------- | ------- | ----------------------------------------------------- | | NETWORK_ERROR | ✅ Yes | Transient connectivity issue | | PROVIDER_SERVER_ERROR | ✅ Yes | Transient 5xx from provider | | PROVIDER_REQUEST_ERROR | ❌ No | 4xx is a caller error (bad input, insufficient funds) | | RATE_EXPIRED | ❌ No | Logic error, not a transient failure | | PROVIDER_NOT_CONFIGURED | ❌ No | Config error |

Default backoff schedule (with defaults retryDelayMs: 500, retryBackoffMultiplier: 2):

Attempt 1 fails → wait 500ms  → Attempt 2
Attempt 2 fails → wait 1000ms → Attempt 3
Attempt 3 fails → wait 2000ms → Attempt 4
Attempt 4 fails → throw RampError

To disable retries entirely, set maxRetries: 0:

const ramp = new NairaRamp({
  quidaxApiKey: '...',
  maxRetries: 0, // fail fast, no retries
});

Core API

ramp.getRate(options)

const rate = await ramp.getRate({
  currency: 'USDT', // 'USDT' | 'USDC'
  type: 'offramp', // 'offramp' | 'onramp'
  provider: 'auto', // optional, overrides defaultProvider
});
// → RateResult { provider, rate, merchantRate, fee, feePercent, expiresAt, raw }

rate is the raw provider exchange rate. merchantRate is the effective rate after any merchantPercent spread configured on the provider — use this when displaying rates to end users. When merchantPercent is unset or 0, merchantRate equals rate.

ramp.offramp(params) — Sell crypto, receive NGN

const tx = await ramp.offramp({
  amount: 100,               // crypto amount
  currency: 'USDT',
  bankAccount: {
    accountNumber: '0123456789',
    bankCode: '058',
    accountName: 'John Doe', // optional
  },
  provider: 'auto',          // optional
  onSuccess: (tx) => { ... },
  onError: (err) => { ... },
  onStatusChange: (status) => { ... },
})
// → Transaction

ramp.onramp(params) — Buy crypto with NGN

const tx = await ramp.onramp({
  amount: 50_000,            // NGN amount
  currency: 'USDT',
  walletAddress: '0xabc...',
  provider: 'auto',
  onSuccess: (tx) => { ... },
  onError: (err) => { ... },
  onStatusChange: (status) => { ... },
})
// → Transaction

ramp.getTransaction(id, provider)

const tx = await ramp.getTransaction('tx-id-123', 'quidax');
// → Transaction

ramp.verifyBankAccount(account, provider?)

const verified = await ramp.verifyBankAccount(
  { accountNumber: '0123456789', bankCode: '058' },
  'yellowcard' // optional, uses defaultProvider if omitted
);
// → BankAccount { accountNumber, bankCode, accountName }

Note: Quidax has no bank lookup endpoint. When Quidax is the active provider, verifyBankAccount returns the input unchanged with accountName as undefined.

Get supported banks

const banks = await ramp.getBanks()
// or specify provider:
const banks = await ramp.getBanks('yellowcard')

console.log(banks)
// [{ name: 'GT Bank', code: '058', country: 'NG' }, ...]

Provider notes:

  • YellowCard: fetches live bank list from the API
  • Quidax: returns a built-in list of major Nigerian banks (no API endpoint available)

Supported networks

// All networks across all configured providers
const all = ramp.getSupportedNetworks()

// Filter by currency
const usdtNetworks = ramp.getSupportedNetworks('USDT')

// Filter by currency and provider
const ycNetworks = ramp.getSupportedNetworks('USDT', 'yellowcard')
// [{ currency: 'USDT', network: 'ethereum', provider: 'yellowcard' }, ...]

Provider network support:

| Provider | USDT | USDC | |------------|------------------------|-----------------------------| | YellowCard | ethereum, tron, bsc | ethereum, base, optimism | | Quidax | ethereum, tron | ethereum |

Pass network to offramp(), onramp(), or getRate() to target a specific chain:

const tx = await ramp.offramp({
  amount: 50000,
  currency: 'USDT',
  network: 'tron',          // optional — routes to a provider that supports USDT on Tron
  bankAccount: { accountNumber: '0123456789', bankCode: '058' },
})

const tx = await ramp.onramp({
  amount: 50000,
  currency: 'USDC',
  network: 'base',
  walletAddress: '0xYourWalletAddress',
})

If the requested network is not supported by any configured provider, a RampError with code NETWORK_NOT_SUPPORTED is thrown.

React Hooks

useRates

import { useRates } from 'naira-ramp';

function RateDisplay({ ramp }) {
  const { rate, loading, error, refetch } = useRates(ramp, {
    currency: 'USDT',
    type: 'offramp',
    refreshIntervalMs: 30_000, // default
  });

  if (loading) return <span>Loading...</span>;
  if (error) return <button onClick={refetch}>Retry</button>;
  return <span>1 USDT = ₦{rate.rate}</span>;
}

useOfframp

import { useOfframp } from 'naira-ramp';

function OfframpForm({ ramp }) {
  const { initiate, status, transaction, loading, error, reset } =
    useOfframp(ramp, { pollIntervalMs: 3000 }); // default 5000ms

  async function handleSubmit() {
    await initiate({
      amount: 100,
      currency: 'USDT',
      bankAccount: { accountNumber: '0123456789', bankCode: '058' },
    });
  }

  return (
    <div>
      <button onClick={handleSubmit} disabled={loading}>
        {loading ? 'Processing...' : 'Send'}
      </button>
      {status && <p>Status: {status}</p>}
      {transaction?.status === 'completed' && (
        <p>Done! Ref: {transaction.reference}</p>
      )}
      <button onClick={reset}>Reset</button>
    </div>
  );
}

useOnramp

Mirror of useOfframp — accepts OnrampParams and calls ramp.onramp() internally. Also accepts an optional { pollIntervalMs } second argument.

useTransactionHistory

import { useTransactionHistory } from 'naira-ramp';

function HistoryPanel({ ramp }) {
  const { transactions, refresh, clear } = useTransactionHistory(ramp);

  return (
    <div>
      {transactions.map((tx) => (
        <div key={tx.id}>
          {tx.reference} — {tx.status} — ₦{tx.amountNGN.toLocaleString()}
        </div>
      ))}
      <button onClick={refresh}>Refresh</button>
      <button onClick={clear}>Clear</button>
    </div>
  );
}

Drop-in Widgets (React web only)

OfframpWidget

import { OfframpWidget } from 'naira-ramp';

// Inline (default)
<OfframpWidget
  ramp={ramp}
  theme='light'           // 'light' | 'dark' | 'auto' (default: 'light')
  variant='inline'        // 'inline' | 'modal' (default: 'inline')
  defaultCurrency='USDT'
  defaultAmount={100}
  onSuccess={(tx) => console.log('Done', tx.reference, tx.txHash)}
  onError={(err) => console.error(err)}
  onClose={() => setVisible(false)}
  style={{ maxWidth: 480 }}
/>;

// Modal — renders a trigger button; clicking opens a full-screen overlay
<OfframpWidget
  ramp={ramp}
  variant='modal'
  triggerLabel='Cash Out'  // label on the trigger button (default: 'Cash Out')
  theme='auto'             // follows OS dark/light mode
  onSuccess={(tx) => console.log(tx)}
/>;

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | ramp | NairaRamp | — | Required. SDK instance. | | theme | 'light' \| 'dark' \| 'auto' | 'light' | Color theme. 'auto' follows OS preference. | | variant | 'inline' \| 'modal' | 'inline' | Inline renders directly; modal renders a trigger button. | | triggerLabel | string | 'Cash Out' | Label for the modal trigger button. | | defaultCurrency | SupportedCurrency | 'USDT' | Pre-selected currency. | | defaultAmount | number | — | Pre-filled crypto amount. | | onSuccess | (tx: Transaction) => void | — | Called when the transaction completes. | | onError | (error: RampError) => void | — | Called on error. | | onClose | () => void | — | Called when the widget is dismissed (close button or Escape). | | style | React.CSSProperties | — | Extra styles on the card container. |

Flow: Enter amount + bank details → Verify account → Confirm rate → Submit → Live status polling → Done.

OnrampWidget

import { OnrampWidget } from 'naira-ramp';

// Inline (default)
<OnrampWidget
  ramp={ramp}
  theme='dark'
  defaultCurrency='USDT'
  onSuccess={(tx) => console.log('Done', tx.reference, tx.txHash)}
/>;

// Modal
<OnrampWidget
  ramp={ramp}
  variant='modal'
  triggerLabel='Buy Crypto'
  theme='auto'
  onSuccess={(tx) => console.log(tx)}
/>;

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | ramp | NairaRamp | — | Required. SDK instance. | | theme | 'light' \| 'dark' \| 'auto' | 'light' | Color theme. 'auto' follows OS preference. | | variant | 'inline' \| 'modal' | 'inline' | Inline renders directly; modal renders a trigger button. | | triggerLabel | string | 'Buy Crypto' | Label for the modal trigger button. | | defaultCurrency | SupportedCurrency | 'USDT' | Pre-selected currency. | | defaultAmount | number | — | Pre-filled NGN amount. | | onSuccess | (tx: Transaction) => void | — | Called when the transaction completes. | | onError | (error: RampError) => void | — | Called on error. | | onClose | () => void | — | Called when the widget is dismissed (close button or Escape). | | style | React.CSSProperties | — | Extra styles on the card container. |

Flow: Enter NGN amount + wallet address → Submit → Live status polling → Done.

Transaction History

Every offramp() and onramp() call is automatically saved to the store. getTransaction() polls update the stored status.

Default — in-memory (zero config)

const ramp = new NairaRamp({ quidaxApiKey: '...' })

const tx = await ramp.offramp({ ... })

ramp.getHistory()   // → Transaction[]
ramp.clearHistory() // → void

Persist across sessions with LocalStorageStore

import { NairaRamp, LocalStorageStore } from 'naira-ramp';

const ramp = new NairaRamp({
  quidaxApiKey: '...',
  store: new LocalStorageStore(), // stored in localStorage
  // store: new LocalStorageStore('my-key') // custom storage key
});

Custom store (e.g. React Native AsyncStorage)

Implement the TransactionStore interface:

import type { TransactionStore, Transaction } from 'naira-ramp'

class MyStore implements TransactionStore {
  save(tx: Transaction): void { ... }
  update(id: string, tx: Transaction): void { ... }
  getById(id: string): Transaction | null { ... }
  getAll(): Transaction[] { ... }
  clear(): void { ... }
}

const ramp = new NairaRamp({ quidaxApiKey: '...', store: new MyStore() })

Error Handling

All errors are instances of RampError:

import { RampError } from 'naira-ramp'

try {
  await ramp.offramp({ ... })
} catch (err) {
  if (err instanceof RampError) {
    console.log(err.code)      // error code (see below)
    console.log(err.provider)  // 'yellowcard' | 'quidax' | undefined
    console.log(err.raw)       // raw API response
  }
}

| Code | Cause | | --------------------------- | ---------------------------------------------------- | | PROVIDER_REQUEST_ERROR | 4xx response from provider | | PROVIDER_SERVER_ERROR | 5xx response from provider | | NETWORK_ERROR | Network timeout or connection failure | | RATE_EXPIRED | Rate expired before transaction was submitted | | PROVIDER_NOT_CONFIGURED | Requested provider has no API key configured | | NO_PROVIDERS_AVAILABLE | All providers failed in auto mode | | WEBHOOK_SIGNATURE_INVALID | Webhook signature verification failed | | WEBHOOK_PAYLOAD_INVALID | Webhook payload missing required fields or malformed | | NETWORK_NOT_SUPPORTED | Requested network not supported by any configured provider |

Webhook Support

Webhooks are server-side only — you need an HTTP endpoint to receive them. They work alongside polling; both can run simultaneously.

ramp.handleWebhook(options)

const tx = await ramp.handleWebhook({
  provider: 'yellowcard', // or 'quidax'
  rawBody: '...', // raw unparsed request body string
  signature: '...', // value from provider's signature header
});
// → Transaction
  • Verifies the signature using HMAC-SHA256 (throws WEBHOOK_SIGNATURE_INVALID on failure)
  • Parses payload and maps to Transaction (throws WEBHOOK_PAYLOAD_INVALID if malformed)
  • Saves/updates the transaction in the store automatically
  • Fires onSuccess, onError, onStatusChange callbacks registered during offramp()/onramp()

Next.js App Router example

// app/api/webhooks/yellowcard/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { ramp } from '@/lib/ramp'; // your NairaRamp singleton

export async function POST(req: NextRequest) {
  const rawBody = await req.text();
  const signature = req.headers.get('x-yc-signature') ?? '';

  try {
    const tx = await ramp.handleWebhook({
      provider: 'yellowcard',
      rawBody,
      signature,
    });
    console.log('Webhook received:', tx.id, tx.status);
    return NextResponse.json({ ok: true });
  } catch (err) {
    return NextResponse.json({ error: 'Invalid webhook' }, { status: 400 });
  }
}
// app/api/webhooks/quidax/route.ts
export async function POST(req: NextRequest) {
  const rawBody = await req.text();
  const signature = req.headers.get('x-quidax-signature') ?? '';

  try {
    const tx = await ramp.handleWebhook({
      provider: 'quidax',
      rawBody,
      signature,
    });
    return NextResponse.json({ ok: true });
  } catch (err) {
    return NextResponse.json({ error: 'Invalid webhook' }, { status: 400 });
  }
}

Signature headers

| Provider | Header | | ---------- | -------------------- | | YellowCard | X-YC-Signature | | Quidax | X-Quidax-Signature |

Sandbox Mode

Run without real API keys using Quidax's mock mode:

const ramp = new NairaRamp({
  sandbox: true,
  quidaxApiKey: 'mock', // any non-empty string
});

Or run the built-in sandbox script:

npm run sandbox

React Native

Hooks work on React Native out of the box:

import { useRates, useOfframp, useOnramp } from 'naira-ramp';

Widgets (OfframpWidget, OnrampWidget) are web-only. Import naira-ramp/native in a React Native context and you'll get a clear error pointing you to the hooks.

Environment Variables

Copy .env.example to .env and fill in your keys:

cp .env.example .env
YELLOWCARD_API_KEY=
YELLOWCARD_SECRET_KEY=
QUIDAX_API_KEY=

Build

npm run build      # compile to dist/
npm run typecheck  # type-check without emitting
npm run sandbox    # run the sandbox demo

License

MIT