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

@usdctofiat/offramp

v4.5.1

Published

USDC-to-fiat offramp SDK — create delegated deposits in one function call

Readme

@usdctofiat/offramp

npm version License: MIT

USDC-to-fiat offramp SDK for Base. Deposit creation, OTC helpers, React hooks, platform/currency catalogs, and typed developer resources.

Agent bundle

If you are integrating through an agent (Claude Code, Cursor, etc.), start here:

The SDK also exports a typed resource map for self-serve products and coding agents:

import { OFFRAMP_DEVELOPER_RESOURCES, getOfframpDeveloperResources } from "@usdctofiat/offramp";

OFFRAMP_DEVELOPER_RESOURCES.delegation.required; // true
OFFRAMP_DEVELOPER_RESOURCES.delegation.feeRateBps; // DELEGATE_MANAGER_FEE_BPS
OFFRAMP_DEVELOPER_RESOURCES.links.agentSkill; // https://usdctofiat.xyz/skills/usdctofiat.md
OFFRAMP_DEVELOPER_RESOURCES.upstreamSourceTruths.map((source) => source.label);
// ["@zkp2p/sdk", "zkp2p/protocol curator", ...]
OFFRAMP_DEVELOPER_RESOURCES.checklist.map((item) => item.title); // integration doctor

const botPlaybook = getOfframpDeveloperResources("bot");
// profile-specific steps + canonical docs/starters resources

Use upstreamSourceTruths before generating protocol-sensitive code. It points agents at the current ZKP2P SDK and zkp2p/protocol curator, attestor, and contracts sources so they do not invent payment-method hashes, proof payload fields, or contract addresses.

For agent-built integrations, copy the same prompt the developer console uses:

import { getOfframpAgentPrompt } from "@usdctofiat/offramp";

const prompt = getOfframpAgentPrompt("bot");

Install

bun add @usdctofiat/offramp

Quick Start

import { offramp, PLATFORMS, CURRENCIES } from "@usdctofiat/offramp";

const result = await offramp(walletClient, {
  amount: "100",
  platform: PLATFORMS.REVOLUT,
  currency: CURRENCIES.EUR,
  identifier: "alice",
});
// { depositId: "362", txHash: "0x...", resumed: false }

Fees

Every deposit created with this SDK is delegated to the Delegate vault's rate manager (the SDK always calls setRateManager to the vault — see How It Works). That rate manager charges a manager fee:

  • Who pays: the buyer (taker). The fee is deducted from the USDC released to them at fill time — it is not added to your deposit cost and is not baked into the displayed rate.
  • Where it goes: the Delegate vault (Galleon Labs).
  • Discover it in code: createOfframp({ walletClient }).getVaultStatus().feeRateBps returns the current feeRateBps (defined by DELEGATE_MANAGER_FEE_BPS in src/config.ts).

The only other attribution the SDK attaches is the on-chain referrer tag referrer: "galleonlabs" (ERC-8021 builder attribution). That is not a fee — no separate referral/referrerFee is configured by this SDK. There is no SDK spread either: getQuote() returns vaultSpreadBps: 0.

Deposit Management

import { deposits, close } from "@usdctofiat/offramp";

const list = await deposits("0xYourAddress");
await close(walletClient, "361");

React

import { useOfframp } from "@usdctofiat/offramp/react";
import { PLATFORMS, CURRENCIES } from "@usdctofiat/offramp";

function SellButton({ walletClient }: { walletClient: WalletClient }) {
  const { offramp, step, isLoading } = useOfframp();

  return (
    <button
      disabled={isLoading}
      onClick={() => offramp(walletClient, {
        amount: "100",
        platform: PLATFORMS.REVOLUT,
        currency: CURRENCIES.EUR,
        identifier: "alice",
      })}
    >
      {step ?? "Sell USDC"}
    </button>
  );
}

Platform & Currency Data

import { PLATFORMS, CURRENCIES } from "@usdctofiat/offramp";

PLATFORMS.REVOLUT.name; // "Revolut"
PLATFORMS.REVOLUT.currencies; // ["USD", "EUR", "GBP", ...]
PLATFORMS.REVOLUT.identifier.label; // "Revtag"
PLATFORMS.REVOLUT.identifier.placeholder; // "revtag (no @)"
PLATFORMS.REVOLUT.identifier.help; // "Revtag without @ (must be public)"
PLATFORMS.REVOLUT.validate("@alice"); // { valid: true, normalized: "alice" }

CURRENCIES.EUR.symbol; // "€"
CURRENCIES.EUR.name; // "Euro"
CURRENCIES.EUR.countryCode; // "eu"

Integration Playbooks

getOfframpDeveloperResources() returns the canonical developer bundle:

  • app: user-facing wallet flows with useOfframp()
  • bot: server-wallet and automation flows
  • agent: skill, llms.txt, and starter-template routing for coding agents
  • private-otc: one-buyer deposits with whitelist enforcement
  • peerlytics: paid protocol API, analytics, orderbook, credits, deposits, and intents

This is intentionally duplicated into the package so integrators can expose docs, help menus, agent context, or onboarding checklists without scraping the site.

OTC Private Orders

Restrict a deposit to a single taker wallet. Pass otcTaker and the deposit is created, delegated, and restricted in one call:

import { offramp, PLATFORMS, CURRENCIES } from "@usdctofiat/offramp";

const { depositId, otcLink } = await offramp(walletClient, {
  amount: "100",
  platform: PLATFORMS.REVOLUT,
  currency: CURRENCIES.EUR,
  identifier: "alice",
  otcTaker: "0xBuyerAddress",
});
// otcLink = "https://otc.usdctofiat.xyz/d/0x.../362"
// Share otcLink with the buyer — they open it, connect their wallet, and fill the order.

Toggle OTC on existing deposits:

import { enableOtc, disableOtc, getOtcLink } from "@usdctofiat/offramp";

await enableOtc(walletClient, "362", "0xBuyerAddress");
await disableOtc(walletClient, "362"); // make public again
getOtcLink("362"); // just the URL, no tx

Resumable

offramp() is idempotent on the delegation step. If an undelegated deposit exists for the wallet, it skips straight to delegation. Handles browser crashes, failed delegation, and retries automatically. Just call offramp() again.

Idempotency & server-side dedup

OfframpParams.idempotencyKey is browser-only: the replay cache is backed by sessionStorage, so it is a no-op in Node and workers — a backend integrator who passes a key gets zero duplicate protection there. It is also honored only by the Offramp class (createDeposit) and the useOfframp hook; the standalone offramp() function ignores it entirely. When you pass a key in a runtime with no sessionStorage, the SDK emits a one-time console.warn so you find out at startup instead of via duplicate deposits.

For server-side dedup, check for an existing open deposit before creating a new one (this is the same idea as the resume behavior above):

import { createOfframp, deposits } from "@usdctofiat/offramp";

const existing = await deposits(walletAddress);
const open = existing.find((d) => d.status === "active" && d.remainingUsdc > 0);

const result = open
  ? open // reuse it — don't create a duplicate
  : await createOfframp({ walletClient }).createDeposit(params);

Error Handling

import { offramp, OfframpError, OFFRAMP_ERROR_CODES } from "@usdctofiat/offramp";

try {
  await offramp(walletClient, params);
} catch (err) {
  if (err instanceof OfframpError) {
    if (err.code === "USER_CANCELLED") return;
    if (err.code === OFFRAMP_ERROR_CODES.EXTENSION_REGISTRATION_REQUIRED) {
      // User needs to register in the Peer extension first — see below.
      return;
    }
    // For any other failure: call offramp() again to resume
  }
}

offramp() already maps wallet rejections to OfframpError with code USER_CANCELLED. If you also make your own wallet calls (a custom approve step, say) or want to inspect OfframpError.cause, use the same matcher the SDK uses internally:

import { isUserCancellation } from "@usdctofiat/offramp";

try {
  await walletClient.writeContract(/* your own call */);
} catch (err) {
  if (isUserCancellation(err)) return; // user dismissed the wallet prompt
  throw err;
}

Peer Extension Registration (PayPal, Wise, Venmo, Cash App)

PayPal, Wise, Venmo, and Cash App makers may need to register their handle inside the Peer (PeerAuth) browser extension before the first deposit. The SDK throws OfframpError with code EXTENSION_REGISTRATION_REQUIRED when curator rejects a maker for this reason. Drive install, connection approval, headless seller-credential capture, seller-credential upload, and retry via the React hook:

import { PLATFORMS, CURRENCIES, OFFRAMP_ERROR_CODES } from "@usdctofiat/offramp";
import { useOfframp, usePeerExtensionRegistration } from "@usdctofiat/offramp/react";

function PayPalSellButton({ walletClient }) {
  const { offramp, lastError } = useOfframp();
  const peer = usePeerExtensionRegistration(PLATFORMS.PAYPAL);
  const depositParams = {
    amount: "100",
    platform: PLATFORMS.PAYPAL,
    currency: CURRENCIES.USD,
    identifier: "alicepay", // PayPal.me username, NOT email
  };

  const needsExtension = lastError?.code === OFFRAMP_ERROR_CODES.EXTENSION_REGISTRATION_REQUIRED;

  return (
    <>
      <button onClick={() => offramp(walletClient, depositParams)}>Sell USDC</button>

      {needsExtension && (
        <div>
          <p>{peer.info?.requiredPrompt}</p>
          {peer.phase === "needs_install" && (
            <button onClick={peer.installExtension}>Install Peer Extension</button>
          )}
          {peer.phase === "needs_connection" && (
            <button onClick={peer.connectExtension} disabled={peer.busy}>
              Connect Peer Extension
            </button>
          )}
          {peer.phase === "ready" && (
            <button onClick={peer.startRegistrationCapture} disabled={peer.busy}>
              {peer.info?.ctaLabel ?? "Register with Peer"}
            </button>
          )}
          {peer.capturedMetadata && !peer.error && (
            <button onClick={() => peer.completeRegistration(walletClient, depositParams)}>
              Continue registration
            </button>
          )}
          {peer.info?.ctaSubtext && <small>{peer.info.ctaSubtext}</small>}
        </div>
      )}
    </>
  );
}

Or drive the handshake manually via peerExtensionSdk. getPeerExtensionRegistrationAuthParams(...) returns the right capture mode for the platform. Venmo and Cash App use seller-credential bundle capture; PayPal and Wise use buyerTee identity-attestation capture.

import {
  CURRENCIES,
  PLATFORMS,
  completePeerExtensionRegistration,
  getPeerExtensionRegistrationAuthParams,
  isPeerExtensionMetadataBridgeAvailable,
  offramp,
  peerExtensionSdk,
} from "@usdctofiat/offramp";

const params = getPeerExtensionRegistrationAuthParams("paypal");
if (!params) throw new Error("PayPal registration is not extension-gated.");
const depositParams = {
  amount: "100",
  platform: PLATFORMS.PAYPAL,
  currency: CURRENCIES.USD,
  identifier: "alicepay",
};

const startCapture = () => {
  const unsubscribe = peerExtensionSdk.onMetadataMessage(async (message) => {
    if (message.platform !== params.platform) return;
    await completePeerExtensionRegistration({
      platform: PLATFORMS.PAYPAL,
      identifier: "alicepay",
      capturedMetadata: message,
      callerAddress: walletAddress,
    });
    await offramp(walletClient, depositParams);
  });
  peerExtensionSdk.authenticate(params);
  return unsubscribe;
};

const state = await peerExtensionSdk.getState();
if (state === "needs_install" || !isPeerExtensionMetadataBridgeAvailable()) {
  peerExtensionSdk.openInstallPage();
} else if (state === "needs_connection") {
  const approved = await peerExtensionSdk.requestConnection();
  if (approved) startCapture();
} else {
  startCapture();
}

For PayPal and Wise, completePeerExtensionRegistration(...) requires callerAddress and posts an identity attestation through /v2/makers/create. For Venmo and Cash App, captured metadata must include sarCredentialCapture.credentialBundle; completion validates the captured offchainId, checks the bundle payee hash, uploads the seller credential bundle, and returns sellerCredentialResponse plus sellerCredentialStatus. After registration succeeds, call offramp() again with the same handle.

Dynamic Taker Tiers

Curator computes taker caps and platform locks dynamically. Use getTakerTier() plus findTakerPlatformLimit() when a take-side UI needs the current limit for a wallet/payment rail:

import { findTakerPlatformLimit, getTakerTier } from "@usdctofiat/offramp";

const tier = await getTakerTier({ owner: takerAddress });
const paypalLimit = findTakerPlatformLimit(tier, { platform: "paypal" });

if (paypalLimit?.isLocked) {
  console.log(`PayPal unlocks at ${paypalLimit.minTierRequired} tier`);
}

How It Works

Every deposit is automatically delegated to the Delegate vault for oracle-based rate management (which charges a manager fee — see Fees), auto-closes when filled, and is attributed to Galleon Labs via ERC-8021.

Testing your integration

There is no public staging or sandbox deployment — the SDK targets Base mainnet and the production curator API. Two ways to test:

  • Small-amount mainnet runs (recommended). Create a 1 USDC deposit against a real payment handle, confirm it on basescan and via deposits(walletAddress), then close() it. This exercises the real approve → register → deposit → delegate path end to end.
  • Proxy or mock the curator REST surface. Pass apiBaseUrl to point the SDK's maker registration/validation calls (/v2/makers/*) at a local proxy or mock. Defaults are byte-for-byte unchanged when omitted. Only the curator REST surface is overridable — the indexer and Base RPC always target mainnet, and on-chain calls still need a real wallet client.
import { createOfframp, PLATFORMS, CURRENCIES } from "@usdctofiat/offramp";

const sdk = createOfframp({
  walletClient,
  apiBaseUrl: "http://localhost:8787", // proxy/mock of https://api.zkp2p.xyz
});

await sdk.createDeposit({
  amount: "1",
  platform: PLATFORMS.REVOLUT,
  currency: CURRENCIES.EUR,
  identifier: "alice",
});

Lifecycle State

Use deposits(address) for seller-owned deposit reconciliation. Use @peerlytics/sdk when your product needs broader deposit, intent, orderbook, or explorer reads.

Links

usdctofiat.xyz · usdctofiat.xyz/delegate · peerlytics.xyz · peerlytics.xyz/orderbook

Examples & agent skills · @usdctofiat