Offramp Integration
As a liquidity provider, you sell USDC on Base for fiat. Use @zkp2p/sdk to create and fund USDC deposits, choose the payment platforms, currencies and rates you accept, and read deposit and intent state. Buyers pay you through those platforms and receive your USDC at your rate.
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,
});
The protocol runs on Base mainnet (chain ID 8453), and the client targets production by default.
Core operations
Approve USDC
Approve the escrow before you create or fund a deposit:
import { getContracts } from "@zkp2p/sdk";
const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base
const { addresses } = getContracts(8453, "production");
const result = await client.ensureAllowance({
token: USDC,
amount: 10_000_000000n, // 10,000 USDC (6 decimals)
spender: addresses.escrow,
maxApprove: false,
});
if (!result.hadAllowance) {
console.log("Approval transaction:", result.hash);
}
Create a deposit
import { Currency } from "@zkp2p/sdk";
const { hash } = await client.createDeposit({
token: USDC,
amount: 10_000_000000n, // 10,000 USDC
intentAmountRange: { min: 100000n, max: 1_000_000000n }, // 0.10 to 1,000 USDC per order
processorNames: ["revolut", "chime"],
payeeData: [
{ offchainId: "your-revtag" }, // Revolut
{ offchainId: "$yourchimesign" }, // Chime
],
conversionRates: [
[{ currency: Currency.EUR, conversionRate: "950000000000000000" }], // 0.95 EUR per USDC
[{ currency: Currency.USD, conversionRate: "1020000000000000000" }], // 1.02 USD per USDC
],
});
processorNames, payeeData and conversionRates line up by index. Rates are fiat per USDC with 18 decimals.
If your @zkp2p/sdk version accepts enableTrustedGroupBypass (0.14.x), pass enableTrustedGroupBypass: false whenever a deposit includes Venmo or PayPal. Otherwise createDeposit() also adds the retired Peer Pay merchant group, whose members skip staking on those methods.
Manage deposit settings
import {
getPaymentMethodsCatalog,
resolveFiatCurrencyBytes32,
} from "@zkp2p/sdk";
const methods = getPaymentMethodsCatalog(8453, "production");
await client.setAcceptingIntents({ depositId: 1n, accepting: true });
await client.setIntentRange({ depositId: 1n, min: 50000n, max: 5_000000n });
await client.setCurrencyMinRate({
depositId: 1n,
paymentMethod: methods.revolut.paymentMethodHash,
fiatCurrency: resolveFiatCurrencyBytes32("EUR"),
minConversionRate: 960000000000000000n, // 0.96 (18 decimals)
});
Rate fields at a glance
Contracts, the indexer and Peer's API expose related rate fields. They are not interchangeable:
| Field | Where it lives | Meaning |
|---|---|---|
minConversionRate | On-chain (EscrowV2) | The depositor's fixed rate floor per payment method and currency. Set at deposit creation or with setCurrencyMinRate(). |
conversionRate / grossRate | Indexer, orderbook | The gross rate max(managerRate, escrowFloor), where escrowFloor = max(minConversionRate, oracle spread rate). It equals minConversionRate only when the deposit has neither a vault manager nor an oracle configuration. |
effectiveConversionRate | Peer's API quotes and orderbook | The taker-facing rate after manager fees. Quotes are filtered and sorted by this rate. |
A configured oracle that is stale or invalid halts new intents even with a fixed floor, and a zero escrow floor halts the pair. With a usable floor, a manager rate of zero disables the pair, and a reverting manager call falls back to the floor. The max() expressions describe the usable-rate case.
For the full on-chain rate resolution and fee math, see Run a Vault.
Fund management
await client.addFunds({ depositId: 1n, amount: 5_000000n });
await client.removeFunds({ depositId: 1n, amount: 1_000000n });
await client.withdrawDeposit({ depositId: 1n });
removeFunds() withdraws part of the unlocked balance. withdrawDeposit() returns all unlocked funds and stops the deposit from accepting intents.
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..."]);
Indexer results can lag a transaction. Confirm writes from the receipt and on-chain reads.
Payment methods
Each payeeData entry has the shape { offchainId, telegramUsername?, metadata?, identityAttestation? }. Put the platform identifier in offchainId.
createDeposit() registers each payee with Peer's API (POST /v2/makers/create) to get its payee hash. To register ahead of time, call registerPayeeDetails() and pass the returned hashes as payeeDetailsHashes; createDeposit() then skips registration. Neither path needs an API key.
| Platform | Key | payeeData example | Notes |
|---|---|---|---|
| Wise | wise | { offchainId: 'your-wisetag' } | Wisetag without @. A new Wisetag needs an identity attestation |
| Venmo | venmo | { offchainId: 'YourVenmoUsername' } | Username without @ |
| Revolut | revolut | { offchainId: 'your-revtag' } | Revtag without @ |
| Cash App | cashapp | { offchainId: 'yourcashtag' } | Cashtag without $ |
| PayPal | paypal | { offchainId: 'yourpaypalmeusername' } | PayPal.me username without paypal.me/. A new username needs an identity attestation |
| Zelle | zelle | { offchainId: 'maker@example.com' } | Lowercase email address |
| Monzo | monzo | { offchainId: 'your-monzo-me-name' } | Monzo.me username |
| Mercado Pago | mercadopago | { offchainId: '0000003100064367123868' } | 22-digit CVU or CBU |
| Chime | chime | { offchainId: '$yourchimesign' } | Lowercase $ChimeSign, including the $ |
| X Money | xmoney | { offchainId: 'yourxusername' } | Lowercase X username without @ |
| UPI | upi | { offchainId: 'name@bank' } | Lowercase UPI ID |
Request identity attestations through the attestation helpers in the Client Reference and pass the result as payeeData[].identityAttestation.
To verify and release incoming payments automatically, connect Seller Autopilot.
PAYMENT_PLATFORMS still lists luxon and n26, but neither is in the production payment-method catalog, and the SDK rejects them before sending a deposit transaction.
Catalogs and helpers
import {
Currency,
currencyInfo,
getCurrencyInfoFromHash,
getPaymentMethodsCatalog,
PAYMENT_PLATFORMS,
PLATFORM_METADATA,
resolveFiatCurrencyBytes32,
} from "@zkp2p/sdk";
const methods = getPaymentMethodsCatalog(8453, "production");
const wiseHash = methods["wise"].paymentMethodHash;
const wiseName = PLATFORM_METADATA["wise"].displayName;
const usdInfo = currencyInfo[Currency.USD];
const usdBytes32 = resolveFiatCurrencyBytes32("USD");
const fromHash = getCurrencyInfoFromHash(usdBytes32);
getContracts(8453, "production") returns { addresses, abis } for the deployed contracts.
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);
}
}
Set SDK log verbosity with setLogLevel("debug" | "info" | "error").
React hooks
@zkp2p/sdk/react wraps each deposit operation in a hook: useCreateDeposit, useAddFunds, useRemoveFunds, useWithdrawDeposit, useSetAcceptingIntents, useSetIntentRange and useSetCurrencyMinRate, among others.
import { useCreateDeposit } from "@zkp2p/sdk/react";
function CreateDepositButton({ client, params }) {
const { createDeposit, isLoading, error, txHash } = useCreateDeposit({
client,
});
return (
<div>
<button disabled={isLoading} onClick={() => createDeposit(params)}>
{isLoading ? "Creating..." : "Create Deposit"}
</button>
{txHash && <p>Transaction: {txHash}</p>}
{error && <p>Error: {error.message}</p>}
</div>
);
}
createDeposit() from the hook resolves to null on failure and sets error. See React Hooks for every hook.