Skip to main content

Offramp Integration

The ZKP2P Client SDK is a TypeScript SDK for liquidity providers who want to offer fiat off-ramp services on Base. Use it to create and manage USDC deposits, configure payment methods and currencies, and query on-chain state with RPC-first reads.

Who is this for?

This SDK is designed for liquidity providers (peers) who want to:

  • Create and manage USDC deposits that accept fiat payments
  • Configure payment methods, currencies, and conversion rates
  • Monitor deposit utilization and manage liquidity
  • Earn fees by providing off-ramp services

Installation

npm install @zkp2p/sdk viem
# or
yarn add @zkp2p/sdk viem
# or
pnpm add @zkp2p/sdk viem

Quick start

Initialize the client

import { Zkp2pClient } from "@zkp2p/sdk";
import { createWalletClient, custom } from "viem";
import { base } from "viem/chains";

const walletClient = createWalletClient({
chain: base,
transport: custom(window.ethereum),
});

const client = new Zkp2pClient({
walletClient,
chainId: base.id,
});

Core operations

Create a deposit

import { Currency } from "@zkp2p/sdk";

const { hash } = await client.createDeposit({
token: "0xUSDC_ADDRESS",
amount: 10000000000n, // 10,000 USDC (6 decimals)
intentAmountRange: { min: 100000n, max: 1000000000n },
processorNames: ["wise", "revolut"],
payeeData: [
{ offchainId: "maker" }, // Wise payee details
{ offchainId: "maker" }, // Revolut payee details
],
conversionRates: [
[{ currency: Currency.USD, conversionRate: "1020000000000000000" }], // 1.02 (18 decimals)
[{ currency: Currency.EUR, conversionRate: "950000000000000000" }], // 0.95 (18 decimals)
],
});

console.log("Deposit created:", hash);

Manage deposit settings

await client.setAcceptingIntents({ depositId: 1n, accepting: true });

await client.setIntentRange({ depositId: 1n, min: 50000n, max: 5000000n });

await client.setCurrencyMinRate({
depositId: 1n,
paymentMethod: "0x...",
fiatCurrency: "0x...",
minConversionRate: 1020000n,
});

Rate fields at a glance

You will see several rate fields across the contracts, indexer, and curator APIs. They are related but not interchangeable:

FieldWhere it livesMeaning
minConversionRateOn-chain (EscrowV2)The depositor's fixed rate floor per (payment method, currency) pair. Set at deposit creation or via setCurrencyMinRate().
conversionRate / grossRateIndexer, orderbookThe manager/oracle-adjusted gross rate: max(managerRate, escrowFloor), where escrowFloor = max(minConversionRate, oracle spread rate). Equals minConversionRate only when the deposit has neither a vault manager nor an oracle config — an oracle-configured deposit surfaces the escrow floor even without a vault.
effectiveConversionRateCurator quotes and orderbookThe taker-facing rate after manager fees, computed by curator. Quote filtering and sorting use this.

For the full on-chain rate resolution and fee math, see Run a Vault.

Fund management

await client.addFunds({ depositId: 1n, amount: 5000000n });
await client.removeFunds({ depositId: 1n, amount: 1000000n });
await client.withdrawDeposit({ depositId: 1n });

Querying on-chain data (RPC-first)

const deposits = await client.getDeposits();
const ownerDeposits = await client.getAccountDeposits("0xOwnerAddress");
const deposit = await client.getDeposit(42n);
const batch = await client.getDepositsById([1n, 2n, 3n]);

const intents = await client.getIntents();
const ownerIntents = await client.getAccountIntents("0xOwnerAddress");
const intent = await client.getIntent("0xIntentHash...");

Indexer queries

const deposits = await client.indexer.getDeposits(
{ status: "ACTIVE", minLiquidity: "1000000", depositor: "0xYourAddress" },
{ limit: 50, orderBy: "remainingDeposits", orderDirection: "desc" },
);

const depositsWithRelations = await client.indexer.getDepositsWithRelations(
{ status: "ACTIVE" },
{ limit: 50 },
{ includeIntents: true, intentStatuses: ["SIGNALED"] },
);

const fulfillments = await client.indexer.getFulfilledIntentEvents(["0x..."]);

Payment methods

Each item in payeeData must line up by index with processorNames. If you pass payeeDetailsHashes to createDeposit(), the SDK uses those hashes directly and skips the curator call. Otherwise the SDK forwards each payeeData object to curator's public /v2/makers/create endpoint to obtain the hashes. Either path works without an API key. You can also call registerPayeeDetails() standalone to get the hashes ahead of time.

The canonical payeeData shape is { offchainId, telegramUsername?, metadata? }. Put the platform-specific identifier in offchainId.

PlatformKeypayeeData exampleNotes
Wisewise{ offchainId: 'your-wisetag' }Pass the Wisetag without @. Wise uses a manual approval flow in curator.
Venmovenmo{ offchainId: 'YourVenmoUsername' }Do not include @. Curator validates the exact Venmo username casing.
Revolutrevolut{ offchainId: 'your-revtag' }Do not include @.
Cash Appcashapp{ offchainId: 'yourcashtag' }Do not include $.
PayPalpaypal{ offchainId: 'yourpaypalmeusername' }Use the PayPal.me username without the paypal.me/ prefix. Requires Peer extension 0.6.3 or newer.
Zellezelle{ offchainId: 'maker@example.com' }Curator expects a lowercase email address.
Monzomonzo{ offchainId: 'your-monzo-me-name' }Use the Monzo.me username only.
Mercado Pagomercadopago{ offchainId: '0000003100064367123868' }CVU must be a valid 22-digit Mercado Pago / bank CVU.
Chimechime{ offchainId: '$yourchimesign' }Include the leading $. Curator expects the value in lowercase.
note

luxon and n26 still appear in the SDK's client-side PAYMENT_PLATFORMS list, but neither is in the production payment-method catalog. The SDK rejects them before submitting a deposit transaction. Don't build against them.

import {
getPaymentMethodsCatalog,
PLATFORM_METADATA,
PAYMENT_PLATFORMS,
} from "@zkp2p/sdk";

console.log(PAYMENT_PLATFORMS);

const methods = getPaymentMethodsCatalog(8453, "production");
const wiseHash = methods["wise"].paymentMethodHash;

const wiseInfo = PLATFORM_METADATA["wise"];
console.log(wiseInfo.displayName);

Currency utilities

import {
Currency,
currencyInfo,
getCurrencyInfoFromHash,
resolveFiatCurrencyBytes32,
} from "@zkp2p/sdk";

const usd = Currency.USD;
const info = currencyInfo[Currency.USD];
const usdBytes = resolveFiatCurrencyBytes32("USD");

Contract helpers

import { getContracts, getPaymentMethodsCatalog } from "@zkp2p/sdk";

const { addresses, abis } = getContracts(8453, "production");
const catalog = getPaymentMethodsCatalog(8453, "production");

Supported networks

NetworkChain IDEnvironment
Base Mainnet8453production
Base Mainnet8453preproduction
Base Mainnet8453staging

Token allowance management

import { getContracts } from "@zkp2p/sdk";

const { addresses } = getContracts(8453, "production");

const result = await client.ensureAllowance({
token: "0xUSDC_ADDRESS",
amount: 10000000000n,
spender: addresses.escrow,
maxApprove: false,
});

if (result.hadAllowance) {
console.log("Already had sufficient allowance");
} else {
console.log("Approval transaction:", result.hash);
}

Error handling

import { ValidationError, NetworkError, ContractError } from "@zkp2p/sdk";

try {
await client.createDeposit({
/* ... */
});
} catch (error) {
if (error instanceof ValidationError) {
console.error("Invalid parameters:", error.message);
} else if (error instanceof NetworkError) {
console.error("Network issue:", error.message);
} else if (error instanceof ContractError) {
console.error("Contract error:", error.message);
}
}

Logging

import { setLogLevel } from "@zkp2p/sdk";

setLogLevel("debug"); // 'debug' | 'info' | 'error'

React hooks

import {
useCreateDeposit,
useAddFunds,
useRemoveFunds,
useWithdrawDeposit,
useSetAcceptingIntents,
useSetIntentRange,
useSetCurrencyMinRate,
} from "@zkp2p/sdk/react";

function DepositManager({ client }) {
const { createDeposit, isLoading, error } = useCreateDeposit({ client });

const handleCreate = async () => {
const result = await createDeposit({
token: "0xUSDC_ADDRESS",
amount: 10000000000n,
intentAmountRange: { min: 100000n, max: 1000000000n },
processorNames: ["wise"],
payeeData: [{ offchainId: "maker@example.com" }],
conversionRates: [
[{ currency: "USD", conversionRate: "1020000000000000000" }],
],
});
console.log("Created deposit:", result.hash);
};

return (
<div>
<button disabled={isLoading} onClick={handleCreate}>
{isLoading ? "Creating..." : "Create Deposit"}
</button>
{error && <p>Error: {error.message}</p>}
</div>
);
}