Client Reference
Reference for Zkp2pClient and the standalone helpers in @zkp2p/sdk. For end-to-end flows, start with Offramp Integration or Onramp Integration.
SDK 0.15.0-rc.11 supports OrchestratorV4 routing and deferred payouts with matching deployed contracts. It can still prepare OrchestratorV3 transactions; preparation does not establish that the retired deployment supports execution. Use OrchestratorV4 for current writes.
Constructor
import { Zkp2pClient } from "@zkp2p/sdk";
const client = new Zkp2pClient({
walletClient,
chainId: 8453,
baseApiUrl: "https://api.zkp2p.xyz",
});
| Field | Required | Description |
|---|---|---|
walletClient | Yes | viem WalletClient with an account |
chainId | Yes | 8453 (Base) |
baseApiUrl | No | Root of Peer's API: https://api.zkp2p.xyz. Set it so signalIntent() can fetch gating signatures; other API calls already default to it. Don't append /v1, /v2, or /v3 |
rpcUrl | No | RPC URL for reads. Defaults to the wallet chain's RPC |
rpcTransport | No | viem Transport for reads; takes precedence over rpcUrl |
runtimeEnv | No | production (default), preproduction, or staging. Selects the contract deployment and default endpoints |
indexerUrl | No | Indexer GraphQL endpoint. Defaults to https://indexer.zkp2p.xyz/v1/graphql in production |
timeouts.api | No | API timeout in milliseconds. Defaults to 15000 |
You don't need an API key. Quotes, gating signatures, payee registration, seller credential uploads, and payment verification all work without credentials. Peer's API accepts browser requests only from allowed origins, except public referral reads; if your origin isn't allowed, call it from your server.
OfframpClient is an alias of Zkp2pClient.
Prepared transactions
Write methods simulate the call, send it, and return the transaction hash without waiting for confirmation. Most also have .prepare(), which returns a PreparedTransaction ({ to, data, value, chainId }) for relayers, smart accounts, batching, or gas estimation.
const prepared = await client.signalIntent.prepare({
depositId: 42n,
amount: 100_000000n,
toAddress: "0xYourRecipientAddress",
processorName: "wise",
payeeDetails:
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
fiatCurrencyCode: "USD",
conversionRate: 1_020000000000000000n,
});
await relayer.submit({
to: prepared.to,
data: prepared.data,
value: prepared.value,
});
createDeposit() has no .prepare(). Use prepareCreateDeposit(), which takes the same params and returns { depositDetails, prepared }.
Deposits
Create a deposit
Approve the token, then create the deposit. ensureAllowance({ token, amount, spender?, maxApprove? }) returns { hadAllowance, hash? }; when it sends an approval, wait for hash to confirm first.
const usdc = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const amount = 1_000_000000n; // 1,000 USDC
await client.ensureAllowance({ token: usdc, amount });
const { hash, depositDetails } = await client.createDeposit({
token: usdc,
amount,
intentAmountRange: { min: 10_000000n, max: 500_000000n },
processorNames: ["wise"],
payeeData: [{ offchainId: "maker@example.com" }],
conversionRates: [
[{ currency: "USD", conversionRate: "1020000000000000000" }],
],
});
| Parameter | Required | Description |
|---|---|---|
token | Yes | Deposit token, such as USDC on Base |
amount | Yes | Total deposit in token base units (USDC has 6 decimals) |
intentAmountRange | Yes | { min, max } amount per intent, in base units |
processorNames | Yes | Payment platforms, such as ["wise", "revolut"] |
conversionRates | Yes | One list of { currency, conversionRate } per platform. Rates have 18 decimals |
payeeData | Unless payeeDetailsHashes is set | One payee per platform; see Payee registration |
payeeDetailsHashes | No | Hashes from registerPayeeDetails(). Skips registration |
delegate | No | Address that may manage the deposit |
intentGuardian | No | Defaults to the deployed IntentGuardian, which lets payers buy more time for a live intent |
retainOnEmpty | No | Keep the deposit open when its balance reaches zero |
referralCode | No | Peer referral code for deposit attribution; see Attribution |
escrowAddress | No | Escrow override |
txOverrides | No | viem transaction overrides plus optional referrer attribution codes |
paymentMethodsOverride, paymentMethodDataOverride, currenciesOverride | No | Raw on-chain values that replace the ones the SDK derives |
depositData | No | Deprecated alias of payeeData |
createDeposit() sends only the deposit transaction. It doesn't configure a whitelist.
After a Venmo or PayPal deposit mines, these releases send one extra transaction per method to add the deposit to a retired access group. Pass enableTrustedGroupBypass: false to createDeposit() to skip it.
Payee registration
createDeposit() registers payeeData with Peer's API. To reuse the hashes, register first with registerPayeeDetails():
const { hashedOnchainIds } = await client.registerPayeeDetails({
processorNames: ["wise", "revolut"],
payeeData: [{ offchainId: "maker@example.com" }, { offchainId: "maker" }],
});
await client.createDeposit({ ...params, payeeDetailsHashes: hashedOnchainIds });
Each payeeData entry is { offchainId, telegramUsername?, metadata?, identityAttestation? }, in the same order as processorNames. PayPal, Wise, and Alipay registrations take an identityAttestation; see Identity attestation. depositData is a deprecated alias.
The returned hash (hashedOnchainId) is the payeeDetails value that deposits, quotes, intents, and seller credentials use.
Manage a deposit
| Method | Parameters | Effect |
|---|---|---|
addFunds | depositId, amount | Add tokens. Approve them first |
removeFunds | depositId, amount | Withdraw part of the available balance |
withdrawDeposit | depositId | Return the remaining balance and stop new intents. Funds locked in live intents stay until they expire; call it again to withdraw them |
setAcceptingIntents | depositId, accepting | Pause or resume new intents |
setIntentRange | depositId, min, max | Change the per-intent limits |
setCurrencyMinRate | depositId, paymentMethod, fiatCurrency, minConversionRate | Change the minimum rate for one platform and currency |
setRetainOnEmpty | depositId, retain | Keep the deposit open at a zero balance |
setDelegate / removeDelegate | depositId, delegate | Let another address manage the deposit |
pruneExpiredIntents | depositId | Return liquidity held by expired intents. Anyone can call it |
Each method accepts optional escrowAddress and txOverrides and has .prepare().
Payment methods and currencies
Platforms and currencies are bytes32 hashes on-chain. Resolve them with resolvePaymentMethodHash("wise") and resolveFiatCurrencyBytes32("USD").
| Method | Parameters | Effect |
|---|---|---|
addPaymentMethods | depositId, paymentMethods, paymentMethodData, currencies | Add platforms to a deposit |
setPaymentMethodActive | depositId, paymentMethod, isActive | Turn a platform on or off |
removePaymentMethod | depositId, paymentMethod | Turn a platform off |
addCurrencies | depositId, paymentMethod, currencies | Add currencies to a platform |
deactivateCurrency / removeCurrency | depositId, paymentMethod, currencyCode | Turn a currency off |
setOracleRateConfig / removeOracleRateConfig | depositId, paymentMethodHash, currencyHash, config | Set or remove oracle pricing; see Oracle helpers |
setOracleRateConfigBatch, updateCurrencyConfigBatch, deactivateCurrenciesBatch | depositId, paymentMethods, and per-currency arrays | Batch versions |
Deposit hooks
OrchestratorV3 has one pre-intent hook slot per deposit. The whitelist-hook methods target OrchestratorV2 deposits. OrchestratorV4 has no deposit hooks.
| Method | Description | Key parameters |
|---|---|---|
setDepositPreIntentHook() / .prepare() | Set the hook called before an intent is signaled | depositId, preIntentHook, escrowAddress?, orchestratorAddress? |
getDepositPreIntentHook() | Read the pre-intent hook | depositId, escrowAddress?, orchestratorAddress? |
setDepositWhitelistHook() / .prepare() | Set the OrchestratorV2 whitelist hook | depositId, whitelistHook, escrowAddress?, orchestratorAddress? |
getDepositWhitelistHook() | Read the OrchestratorV2 whitelist hook | depositId, escrowAddress?, orchestratorAddress? |
cleanupOrphanedIntents() / .prepare() | Clean up orphaned V2 or V3 intents; anyone can call it | intentHashes, escrowAddress?, orchestratorAddress? |
Identity attestation
Some platforms need a verified account identity before payee registration. Request one from the attestation service with the Nitro client the SDK re-exports, or with the lower-level apiRequestIdentityAttestation().
| Platform | Action type | Encrypted session material | Public params |
|---|---|---|---|
venmo | register_venmo | Cookie | { SENDER_ID } |
paypal | register_paypal, register_business_paypal | Cookie | {} |
wise | register_wise | Cookie, X-Access-Token | { PROFILE_ID } |
cashapp | register_cashapp | sessionCookie, requestPayload, optional requestHeaders | {} |
alipay | register_alipay | Cookie | {} |
For Venmo, send only the Cookie header and params.SENDER_ID; don't include a captured URL.
import { createNitroAttestationClient } from "@zkp2p/sdk";
const nitro = createNitroAttestationClient({
environment: "production",
attestationServiceUrl: "https://attestation-service.zkp2p.xyz",
});
const identity = await nitro.requestIdentityAttestation({
platform: "venmo",
actionType: "register_venmo",
callerAddress: "0x0000000000000000000000000000000000000002",
sessionMaterial: {
Cookie: "venmo-session-cookie-header",
},
params: {
SENDER_ID: "123456789",
},
});
console.log(identity.identity.payeeIdHash);
If you encrypted the session material yourself, call the endpoint helper:
import { apiRequestIdentityAttestation } from "@zkp2p/sdk";
const response = await apiRequestIdentityAttestation(
{
callerAddress: "0x0000000000000000000000000000000000000002",
encryptedSessionMaterial,
params: { SENDER_ID: "123456789" },
},
"https://attestation-service.zkp2p.xyz",
"venmo",
"register_venmo",
);
callerAddress is required and signed into the attestation. Payee registration doesn't check it against the caller's wallet.
Intent operations
signalIntent() / signalIntent.prepare()
Reserves liquidity from a deposit and returns the transaction hash. Read the intent hash from the IntentSignaled event (example).
Without gatingServiceSignature and signatureExpiration, the SDK fetches them from Peer's API; baseApiUrl must be set. On OrchestratorV3, a deposit without a gating service is signaled with no signature. OrchestratorV4 requires a signature on every intent.
| Parameter | Required | Description |
|---|---|---|
depositId | Yes | Deposit ID |
amount | Yes | Token amount in base units. From a quote, use signalIntentAmount |
toAddress | Yes | Recipient of the tokens |
processorName | Yes | Payment platform, such as wise |
payeeDetails | Yes | Payee hash of the deposit's payment method |
fiatCurrencyCode | Yes | Fiat code such as USD, or its bytes32 hash |
conversionRate | Yes | Agreed rate with 18 decimals |
referralFees | No | { recipient, fee }[]. fee is a fraction with 18 decimals (10n ** 16n is 1%) |
referrerFeeConfig | No | One { recipient, feeBps }. Can't be combined with referralFees, referrer, or referrerFee |
referrer / referrerFee | No | Deprecated single-referrer fields |
gatingServiceSignature, signatureExpiration | No | A gating signature you already have |
payerCommitment | No | OrchestratorV4 only: bytes covered by your own gating signature. Defaults to 0x |
groupId | No | OrchestratorV4 only: an allowed group the taker signals through. Defaults to zero |
postIntentHook, preIntentHookData, data | No | OrchestratorV3 only. OrchestratorV4 rejects non-empty values |
escrowAddress, orchestratorAddress | No | Routing overrides. Must be the current deployment |
txOverrides | No | viem transaction overrides plus optional referrer attribution codes |
Use OrchestratorV4 for new intents. OrchestratorV3 was retired on 2026-11-05, and its intents and events remain readable. SDK 0.15.0-rc.11 still routes existing intents to their owning orchestrator and can prepare O3 transactions; it does not reject them solely because O3 is retired.
cancelIntent() / cancelIntent.prepare()
Cancels a signaled intent and returns its liquidity to the deposit. The intent owner can call it; on OrchestratorV4, so can the intent's arbitrator.
| Parameter | Required | Description |
|---|---|---|
intentHash | Yes | 0x-prefixed 32-byte intent hash |
orchestratorAddress | No | Orchestrator override |
txOverrides | No | viem transaction overrides |
fulfillIntent() / fulfillIntent.prepare()
Fulfills an intent with a payment proof. The SDK reads the intent, gets a payment attestation from the attestation service, and submits it.
| Parameter | Required | Description |
|---|---|---|
intentHash | Yes | 0x-prefixed 32-byte intent hash |
proof | Unless precomputedAttestation is set | Buyer TEE proof: { proofType: "buyerTee", encryptedSessionMaterial, params }. Legacy zkTLS proofs are rejected |
timestampBufferMs | No | Allowed payment timestamp variance in milliseconds. Defaults to "300000" |
attestationServiceUrl | No | Defaults to https://attestation-service.zkp2p.xyz in production |
attestationServiceFallbackUrls | No | Origins to try after network failures. Omit to use the attestor's proxy; [] disables fallback |
orchestratorAddress | No | Orchestrator override |
postIntentHookData | No | OrchestratorV3 only. OrchestratorV4 rejects non-empty data |
txOverrides | No | viem transaction overrides |
callbacks | No | onAttestationStart, onAttestationComplete, onTxSent, onTxMined |
precomputedAttestation | No | { paymentProof, verificationData } you already encoded |
To build a Buyer TEE proof in a web app, see Onramp Integration. getFulfillIntentInputs(intentHash) returns the intent fields the attestation uses.
releaseFundsToPayer() / releaseFundsToPayer.prepare()
Releases an intent's funds to the payer without a payment proof, for example after the depositor confirms payment some other way. The depositor can call it; on OrchestratorV4, so can the intent's arbitrator.
| Parameter | Required | Description |
|---|---|---|
intentHash | Yes | 0x-prefixed 32-byte intent hash |
releaseAmount | Yes | Amount to release. The depositor must release the full intent amount; an OrchestratorV4 arbitrator may release less, and the rest returns to the deposit. Ignored before OrchestratorV4 |
orchestratorAddress | No | Orchestrator override |
txOverrides | No | viem transaction overrides |
extendIntent() / extendIntent.prepare()
OrchestratorV4 only. Lets the intent's arbitrator extend its expiry at no cost. Takes intentHash, additionalTime in seconds, and optional orchestratorAddress and txOverrides.
Intent lifetime extension
The IntentGuardian lets any payer buy more time for a live intent. Check hasIntentGuardian() first. The fee can change at any time, so quote right before signing and pass the quote's cost as maxCost.
| Method | Purpose |
|---|---|
hasIntentGuardian() | Whether a guardian is deployed; doesn't throw |
getIntentGuardianPolicy() | { guardian, extensionsEnabled, extensionFeeBpsPerHour, maxExtensionFeeBpsPerHour, maxTotalIntentLifetime, ... } |
quoteIntentExtension() | { intentAmount, additionalTime } in, { cost, ... } out |
getIntentGuardianPayerFunding() | { payer, token } in, the payer's balance and allowance out |
extendIntentLifetime() / .prepare() | Pay for and apply the extension |
extendIntentLifetime() requires escrow, depositId, intentHash, additionalTime in seconds, and maxCost. Approve the deposit token for policy.guardian first. The guardian charges the deposit's token, pays the deposit owner, and doesn't refund extensions.
classifyIntentGuardianError(error) maps a failure to EXTENSIONS_DISABLED, COST_EXCEEDS_MAX, FEE_EXCEEDS_INTENT_AMOUNT, INTENT_ALREADY_EXPIRED, ESCROW_NOT_WHITELISTED, ZERO_ADDRESS, or UNKNOWN.
Quote API
Both quote methods call Peer's API without credentials. Request fields go in the first argument; the optional second argument only overrides transport settings.
getQuote(req, opts?)
Finds liquidity for selected payment platforms. Unknown platform names throw before the request.
| Request field | Type | Required | Description |
|---|---|---|---|
paymentPlatforms | string[] | Yes | Platforms to search, such as ["wise", "revolut"] |
fiatCurrency | string | Yes | Fiat currency code, such as USD |
user | string | Yes | Taker address. Whitelist access is checked for this address |
recipient | string | Yes | Address that receives the tokens |
destinationChainId | number | Yes | 8453 (Base) |
destinationToken | string | Yes | Token address |
amount | string | Yes | Positive integer string in the token's base units, in both modes. With USDC, "100000000" means 100 USD with isExactFiat: true, or 100 USDC with isExactFiat: false |
isExactFiat | boolean | No | true (default) fixes the fiat amount and quotes tokens. false fixes the token amount and quotes fiat |
quotePreference | QuotePreference | No | Seller Autopilot or Buyer TEE pool; see preferences |
quotesToReturn | number | No | Maximum quotes, a positive integer. The API returns at most 20 |
escrowAddresses | string[] | No | Escrows to search. Defaults to the client's escrow |
excludedPayToValues | string[] | No | Payee hashes to skip, such as a maker already tried for this order. At most 100 |
includeNearbyQuotes | boolean | No | When nothing matches exactly, also return quotes at nearby amounts. Defaults to false |
nearbySearchRange | number | No | Maximum deviation for nearby quotes, in percent (1 to 100). No limit when omitted |
nearbyQuotesCount | number | No | Nearby quotes per direction, 1 to 10. Defaults to 3 |
referralFees | ReferrerFeeConfig[] | No | Up to five { recipient, feeBps }. Takes precedence over referrerFeeConfig, even when empty |
referrerFeeConfig | ReferrerFeeConfig | No | One { recipient, feeBps }, sent as a one-entry referralFees |
referrer | string | No | Referrer attribution |
useMultihop | boolean | No | Peer's API rejects true; omit it |
Signal with the same referral fees you quoted with. A quote doesn't reserve liquidity or stake; both can change before you signal.
Quote preferences
quotePreference picks between Seller Autopilot (SAR) liquidity, where the seller's credential verifies the payment, and Buyer TEE liquidity, where the buyer proves it.
quotePreference | Search behavior |
|---|---|
| Omitted | Buyer TEE pool only |
EXCLUSIVE_SAR | Seller Autopilot pool only |
PREFER_SAR | Seller Autopilot first, then Buyer TEE as fallback |
PREFER_BUYER_TEE | Buyer TEE first, then Seller Autopilot as fallback |
EXCLUSIVE_BUYER_TEE | Buyer TEE pool only |
For getQuote, a pass with exact matches ends the search; the fallback pool doesn't top up the count. For getQuotesBestByPlatform, the fallback fills platforms that had no best quote.
Per-call options
| Option | Type | Description |
|---|---|---|
baseApiUrl | string | API root for this call. Defaults to the client's baseApiUrl |
timeoutMs | number | Timeout in milliseconds. Defaults to the client's timeouts.api |
import type { QuoteRequest } from "@zkp2p/sdk";
const taker = "0x0000000000000000000000000000000000000001";
const request = {
paymentPlatforms: ["wise"],
fiatCurrency: "USD",
user: taker,
recipient: taker,
destinationChainId: 8453,
destinationToken: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
amount: "100000000", // 100 USD, in 6-decimal units
isExactFiat: true,
quotePreference: "PREFER_SAR",
quotesToReturn: 3,
} satisfies QuoteRequest;
const quote = await client.getQuote(request, { timeoutMs: 15_000 });
const [best] = quote.responseObject.quotes;
if (best) {
await client.signalIntent({
depositId: BigInt(best.intent.depositId),
amount: best.signalIntentAmount ?? best.intent.amount,
toAddress: taker,
processorName: best.intent.processorName,
payeeDetails: best.intent.payeeDetails,
fiatCurrencyCode: "USD",
conversionRate: best.conversionRate,
});
}
Quote responses
getQuote() returns { success, message, statusCode, responseObject }:
| Field | Description |
|---|---|
quotes | Matching quotes. The SDK drops quotes whose payment method it doesn't recognize |
nearbySuggestions | With includeNearbyQuotes and no exact match: { below, above }, each entry a quote plus the suggested amount and its percentage difference |
fiat, token, fees | Currency details, token metadata (including decimals), and the protocol fee |
quoteExpiresAt | ISO 8601 time after which to fetch a new quote |
orchestratorAddress | Orchestrator the quoted intents are signaled on |
Each quote includes:
intent:depositId,escrowAddress,processorName,amount,toAddress,payeeDetails,fiatCurrencyCode, andchainId.signalIntentAmount: the gross token amount to pass tosignalIntent().fiatAmount,tokenAmount, and theirFormattedvariants;conversionRatewith 18 decimals.- Fee details:
serviceFeeBps,zkp2pFeeAmount,takerReferralFeeBps,takerReferralFeeAmount,managerFeeAmount, andtotalFeeAmount, withFormattedvariants. whitelistEnabledandallowedGroupIdsfor the deposit's whitelist.disputeProtectionRequiresStake: the payout may need taker stake or be held; see Taker staking.sellerAutomatedReleaseAvailable: the maker's Seller Autopilot credential is live.
With referrerFeeConfig, the SDK also adds referrerFeeAmount, referrerFeeAmountFormatted, and referrerFeeBps. They aren't added for referralFees.
getQuotesBestByPlatform(req, opts?)
Returns the best quote for each supported platform. Takes the same fields as getQuote except paymentPlatforms, quotesToReturn, useMultihop, and the nearby-quote options, and adds:
| Request field | Type | Description |
|---|---|---|
minDepositSuccessRateBps | number | Minimum maker success rate, 0 to 10000 basis points |
supportBusinessAccounts | boolean | Include business accounts. Defaults to false |
The response has platformQuotes instead of quotes. Each entry is { platform, supported, available, bestQuote? }; check available before using bestQuote.
const best = await client.getQuotesBestByPlatform({
fiatCurrency: "USD",
user: "0x0000000000000000000000000000000000000001",
recipient: "0x0000000000000000000000000000000000000002",
destinationChainId: 8453,
destinationToken: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
amount: "100000000", // receive exactly 100 USDC
isExactFiat: false,
quotePreference: "PREFER_BUYER_TEE",
});
for (const { platform, available, bestQuote } of best.responseObject
.platformQuotes) {
if (available && bestQuote) {
console.log(platform, bestQuote.intent.depositId, bestQuote.fiatAmount);
}
}
Deposit access policy
client.accessPolicy reads deposit whitelists and unwinds whitelists created before access groups were retired. It uses WhitelistPolicyV2 and AddressGroupRegistryV4 with OrchestratorV4, and WhitelistPolicy and AddressGroupRegistry with OrchestratorV3. Where they aren't deployed, calls throw AccessPolicyUnsupportedError; check accessPolicy.isSupported first.
| Method | Purpose |
|---|---|
isEnabled({ escrow, depositId, paymentMethod }) | Whether the whitelist is on. With OrchestratorV4 it covers the whole deposit |
isGroupAllowed({ escrow, depositId, paymentMethod, groupId }) | Whether one group is allowed |
isWhitelisted({ escrow, depositId, taker, groupId? }) | Whether a taker is admitted. A zero groupId checks a direct grant; a nonzero group must be allowed and contain the taker |
groupExists(groupId), isMember({ groupId, account }) | Group registry reads |
getGroup(groupId) | OrchestratorV4 registry only: the group, including its arbitrator |
getAllowedGroups(...), maxGroupsPerDepositPaymentMethod() | OrchestratorV3 only. OrchestratorV4 groups can't be enumerated; use point reads or indexed events |
prepareDisable, prepareRemoveAllowedGroups, prepareRemoveWhitelistedAddresses | Prepared transactions that turn a whitelist off or remove entries. Only the depositor can send them |
A group's arbitrator can release part of, cancel, or extend an intent signaled through that group. With the whitelist off, OrchestratorV4 admits any taker and attaches no arbitrator. Existing whitelists stay enforced until they're disabled or the deposit closes.
Vault and rate-manager operations
The client exposes vaults as rate managers. Use them to delegate deposits or share pricing.
Create a vault
Use createRateManager() to create a vault.
| Field | Required | Description |
|---|---|---|
config.manager | Yes | Manager address |
config.feeRecipient | Yes | Address that receives manager fees |
config.maxFee | Yes | Maximum allowed fee |
config.fee | Yes | Current fee |
config.depositHook | No | Deposit hook contract |
config.minLiquidity | No | Minimum USDC liquidity required for delegation |
config.name | Yes | Display name |
config.uri | Yes | Metadata URI |
txOverrides | No | viem transaction overrides |
Delegation methods
| Method | Use it when | Key parameters |
|---|---|---|
setDepositRateManager() | Delegating through the registry and controller | escrow, depositId, registry, rateManagerId |
clearDepositRateManager() | Clearing controller-based delegation | escrow, depositId |
setRateManager() | Writing directly to EscrowV2 | depositId, rateManagerAddress, rateManagerId, escrowAddress? |
clearRateManager() | Clearing direct EscrowV2 delegation | depositId, escrowAddress? |
Vault configuration
| Method | Description | Key parameters |
|---|---|---|
setVaultFee() | Update the manager fee | rateManagerId, newFee |
setVaultMinRate() | Set the floor rate for one platform and currency | rateManagerId, paymentMethodHash, currencyHash, rate |
setVaultMinRatesBatch() | Batch version of setVaultMinRate() | rateManagerId, paymentMethods, currencies, rates |
setVaultConfig() | Update manager, fee recipient, hook, name, or URI | rateManagerId, newManager, newFeeRecipient, newHook?, newName, newUri |
Rate-manager reads
| Method | Returns | Notes |
|---|---|---|
getDepositRateManager(escrow, depositId) | { registry, rateManagerId } | Current delegation |
getManagerFee(escrow, depositId) | bigint | Effective manager fee |
getEffectiveRate({ escrow, depositId, paymentMethod, fiatCurrency }) | bigint | EscrowV2 rate after the manager's adjustments |
Taker staking and dispute-protection risk
Stake to Take protects sellers on payment methods where the buyer can reverse the fiat payment. On OrchestratorV4, an intent is protected when its payment method has a positive dispute window (14 days by default for Venmo and PayPal), the deposit token is USDC, and the deposit's whitelist is off. At fulfillment, the taker's stake owner locks stake for the window or, without enough free stake, the payout is held in the StakeVault until the window ends. Other intents pay out immediately. The attestor signs each payment's actual window.
DISPUTE_WINDOW_POLICY and getDefaultDisputeWindow(paymentMethod) expose the default windows in seconds. mayRequireDisputeProtection({ paymentMethod, depositToken, stakeToken, whitelistEnabled }) applies the rule above at quote time.
StakeVault collateral belongs to an effective stake owner: the taker itself, or an owner that has authorized the taker. Read getStakeOwner(taker) right before attributing capacity; an expired or revoked selection falls back to the taker's own stake.
Stake and policy reads
| Method | Returns or purpose |
|---|---|
getStakeBalance(stakeOwner) | Total stake owned |
getLockedStake(stakeOwner) | Stake locked by open protections |
getFreeStake(stakeOwner) | Stake available for new protected intents or withdrawal |
getClaimable(beneficiary) | USDC already released into the beneficiary's claim balance |
getStakeOwner(taker) | Effective owner after authorization checks |
getSelectedStakeOwner(taker) | Raw selection; the zero address means none |
getTakerAuthorization(stakeOwner, taker) | Whether the owner currently sponsors the taker |
getStakeVaultState({ staker, taker? }) | Stake, ownership, authorization, and claimable balance in one read |
getAdmissionsPaused() | OrchestratorV3 stack only: whether new protected intents are paused |
getRiskWindow(paymentMethodHash) | OrchestratorV3 stack only: collateral lock window in seconds |
isDisputeProtectionEnabled(escrow, depositId, paymentMethod) | OrchestratorV3 stack only: whether the deposit method uses protection |
Stake methods use the active vault. Pass stakeVaultAddress to read from or withdraw from an earlier known vault; getKnownStakeVaults() lists them and getActiveStakeVaultContract() returns the active one.
Stake and keeper writes
| Method | Signer and behavior |
|---|---|
ensureStakeAllowance({ amount }) | Approves the StakeVault to spend USDC when needed; returns { hadAllowance, hash? } |
depositStake({ amount }) | Deposits USDC; the sender becomes the stake owner |
withdrawStake({ amount }) | Withdraws immediately, up to the free stake |
withdrawStakeTo({ to, amount }) | StakeVaultV2 only: withdraws free stake to another address |
claim() | Withdraws the caller's whole claimable balance |
setTakerAuthorization({ taker, authorized }) | Signed by the stake owner: sponsor a taker or stop |
selectStakeOwner({ stakeOwner }) | Signed by the taker: use an owner that authorizes it |
clearStakeOwner() | Signed by the taker: go back to its own stake |
setDisputeProtectionEnabled({ escrow, depositId, paymentMethod, enabled }) | O3-only write retained in SDK 0.15.0-rc.11; do not use for current integrations |
releaseMaturedDisputeProtectionIntent() / releaseMaturedDisputeProtectionIntents() | OrchestratorV3 stack only: release matured protections; anyone can call it. Takes a disputeProtectionPolicyAddress you pin |
release({ intentHashes }) | OrchestratorV4: release matured protections; see Deferred payouts |
Every write except ensureStakeAllowance() has .prepare(). There is no withdrawal request or cooldown.
Indexed state and risk helpers
client.indexer.getStakingState({ chainId, environment, vaultAddress, disputeProtectionPolicyAddress, taker, stakeOwner }) returns indexed ownership, balances, claims, authorizations, risk windows, and per-row freshness. environment is base or base_staging. Pass a stakeOwner you just read on-chain; if the index hasn't caught up with it, the result reports zero capacity instead of another account's stake.
Pure helpers that match the contract's admission rules:
| Helper | Result |
|---|---|
calculateRequiredCoverage(amount, disputeProtected) | The full intent amount for protected methods, otherwise zero |
calculateStakeBackedCapacity(freeStake) | The largest protected amount the free stake supports |
selectRiskMode(amount, freeStake, disputeProtected) | UNBONDED, STAKE_BACKED, or an insufficient-stake error |
Deployment readiness
getDisputeProtectionReadiness({ verifyPassiveSuccessor? }) checks the OrchestratorV3 dispute-protection deployment against the manifest packaged with the SDK, with every read pinned to one block. It doesn't cover OrchestratorV4.
| State | Meaning |
|---|---|
active_successor_verified | The manifest's successor hook and its full policy and vault stack are active |
recognized_predecessor | The manifest's predecessor hook is still active |
passive_successor | The predecessor is active and the requested successor check passed |
verified_no_hook | OrchestratorV3 has no lifecycle hook |
mismatch | Build identity, runtime code, wiring, or an RPC read failed verification |
readyForDisputeProtection is true only when the orchestrator and policy also accept new protected intents. Local source builds always return mismatch. Published packages expose their build identity as SDK_BUILD_METADATA and @zkp2p/sdk/build-metadata.json.
For dispute evidence, findDisputeProtectionIntent({ publicClient, chainId, environment?, intentHash, blockNumber? }) finds the policy that holds an intent's protection, and readDisputeAttestationConfig({ publicClient, chainId, environment?, policyAddress? }) reads that policy's attestation witnesses and signature threshold at one block.
Deferred payouts
When an OrchestratorV4 payout is held, the StakeVault keeps it under a lock keyed by the intent hash until the dispute window ends.
| Method | Purpose |
|---|---|
getCurrentOrchestratorContract() | Orchestrator for new intents: V4 where deployed, otherwise V3 |
resolveIntentOrchestrator(intentHash) | Orchestrator that owns an existing intent |
getPayoutStatus(intentHash) / getPayoutStatuses(intentHashes) | Payout state per intent |
parseDeferredPayoutFromLogs(logs, intentHash) | The IntentPayoutDeferred event from a fulfillment receipt, or null |
release({ intentHashes, disputeProtectionPolicyAddress?, txOverrides? }) | Release matured protections; anyone can call it. Has .prepare() |
getDisputeProtectionStacks() | { current, draining }: discovers the OrchestratorV4 and historical OrchestratorV3 stacks; either can be null if unavailable |
| Payout state | Meaning |
|---|---|
PENDING | No indexed fulfillment yet |
PAID | Paid at fulfillment |
HELD | Held until maturesAt |
CLAIMABLE | Matured; waiting for release() |
RELEASED | The lock closed: paid to the recipient by release(), or awarded to the depositor in a dispute |
HELD and CLAIMABLE include vault, recipient, amount, and maturesAt.
release() pays each held payout to its recipient, whoever sends the transaction, and returns locked stake to its owner's free stake. It reverts with NotMatured before maturity and ProtectionNotFound after a release or dispute. Intents on the OrchestratorV3 stack use releaseMaturedDisputeProtectionIntent(s) instead.
Referral account APIs
Referral codes belong to wallet addresses. Reads are public and accept requests from any browser origin. Writes are EIP-712 signatures from the client's walletClient, so no Peer account is needed.
| Method | Description |
|---|---|
getReferralDashboard({ address }) | A wallet's code, redemption state, reward rates, referee counts, and lifetime fees |
getReferralEarnings({ address }) | A wallet's earnings breakdown |
lookupReferralCode(code) | { code, referrerWalletAddress, isActive } |
createReferralCodeWithSignature(opts?) | Create the wallet's code, or return its existing one |
redeemReferralCodeWithSignature(code, opts?) | Redeem another wallet's code. Without referrerWalletAddress, looks the code up first and rejects it if inactive |
updateReferralCodeWithSignature(code, { oldCode }) | Rename the wallet's code. oldCode prevents replaying a stale rename |
const dashboard = await client.getReferralDashboard({
address: "0x1111111111111111111111111111111111111111",
});
const lookup = await client.lookupReferralCode("PEER42");
const { code: myCode } = await client.createReferralCodeWithSignature();
await client.redeemReferralCodeWithSignature("PEER42", {
referrerWalletAddress: lookup.referrerWalletAddress,
});
await client.updateReferralCodeWithSignature("MYCODE", {
oldCode: myCode,
});
The signature domain is { name: "ZKP2PReferral", version: "1" } (REFERRAL_SIGNATURE_DOMAIN and REFERRAL_SIGNATURE_TYPES). The SDK signs the current Unix time as issuedAt and the audience base_production in production; override them with the issuedAt and audience options. Sign right before sending, because Peer's API rejects stale signatures. Codes are six letters or digits and are uppercased before signing; isValidReferralCode() and normalizeReferralCode() apply the same rules.
Seller Autopilot
Seller Autopilot verifies payments from the seller's side, so buyers don't have to prove them. The seller connects a payment account once. The attestation enclave seals the credential into an encrypted bundle that Peer's API stores but can't read, and later uses it to check payments. See Seller Autopilot for the trust model.
| Platform | How the seller connects |
|---|---|
venmo, paypal, xmoney | A receipt inbox. Venmo and X Money can use Peer's hosted linking page. Any of the three can connect iCloud Mail with uploadICloudMailSellerCredential() |
cashapp, upi | Session capture in an extension, then uploadSellerCredentialBundle(), or uploadSellerCredential() from a trusted runtime |
wise | A Wise Personal API Token, through uploadSellerCredential() |
Every flow needs a registered payee: the payeeDetails hash from registerPayeeDetails() or createDeposit(). A connection is live when getSellerCredentialStatus() returns status: "active"; for an inbox, the credentialType must also pass isMailboxSellerCredentialType().
uploadSellerCredential()
uploadSellerCredential(params, opts?) has the attestation service sign a credential bundle, registers the payee if needed, and stores the bundle with Peer's API. Run it only where the seller's session material is safe, such as an extension background or your backend. The response's responseObject is the credential status.
platform | Other params |
|---|---|
cashapp | offchainId, payeeId, sessionMaterial, and optional telegramUsername, metadata, callerAddress |
wise | sessionMaterial: { apiToken, profileId? } and optional callerAddress. The enclave derives the payee hash from the token. If the token has several profiles and you omit profileId, handle the profile-selection response |
upi | callerAddress and sessionMaterial: { sessionCookie, userAgent } from Amazon Pay. The attestor reads the receiving UPI ID, and the SDK registers it |
Cash App sessionMaterial is { recipientCashtag, customerId, sessionCookie, requestPayload, requestHeaders? }.
| Option | Description |
|---|---|
baseApiUrl | API root override |
attestationServiceUrl | Defaults to the attestor that matches baseApiUrl |
attestationServiceFallbackUrls | Origins to try after network failures. Omit to use the attestor's proxy; [] disables fallback |
timeoutMs | Timeout in milliseconds |
attestationRuntime | fetch, subtle, or getRandomValues overrides for runtimes without WebCrypto, such as React Native |
const response = await client.uploadSellerCredential(
{
platform: "cashapp",
offchainId: "peer-seller",
payeeId: "123456789",
sessionMaterial: {
recipientCashtag: "peer-seller",
customerId: "123456789",
requestPayload: capturedRequestBody,
sessionCookie: "session_cookie",
requestHeaders: {
"user-agent": "Mozilla/5.0",
},
},
},
{ timeoutMs: 10_000 },
);
uploadSellerCredentialBundle()
Use uploadSellerCredentialBundle(params, opts?) when an extension already created the encrypted bundle, for example with apiCreateSellerCredentialBundle(). This is the page side of the extension Seller Autopilot flow.
For cashapp, upi, and venmo, pass platform, offchainId, bundle, and optional telegramUsername and metadata. For wise, pass platform and bundle. opts takes baseApiUrl and timeoutMs.
The SDK registers the payee, checks that the registered hash equals bundle.payeeIdHash, then stores the bundle. The check stops a tampered capture from binding a credential to a different payee.
const response = await client.uploadSellerCredentialBundle({
platform: "cashapp",
offchainId: capture.offchainId,
bundle: capture.credentialBundle,
});
Hosted Venmo receipt linking
Peer hosts a page where a seller links Gmail, personal Outlook, or iCloud Mail to a registered Venmo payee. The page runs the consent flow and collects the iCloud app-specific password; your app never receives OAuth codes, passwords, tokens, or email contents. For a cash-out product, @zkp2p/cash wraps this flow and also handles registration.
| Helper | Use |
|---|---|
openVenmoGmailConnect({ payeeDetails, payeeUsername?, peerOrigin?, appearance?, popup? }) | Browsers. Call it directly from a click handler. Opens a popup on desktop and a tab on mobile, then resolves { payeeDetails } |
getVenmoGmailConnectUrl({ payeeDetails, payeeUsername?, peerOrigin?, appearance?, returnUrl? }) | Native apps and custom windows. Returns the page URL; returnUrl sends the seller back to your app |
payeeUsername prefills the iCloud Mail step. peerOrigin defaults to https://app.peer.xyz. For styling, see popup customization.
openVenmoGmailConnect() rejects with VenmoGmailConnectError. Its code is popup_blocked, connection_closed, or connection_timeout (after 10 minutes), or an error code from the page, such as venmo_google_oauth_receipt_not_found or icloud_receipt_not_found.
A closed window or a return to your app doesn't prove the link worked. Read getSellerCredentialStatus() afterwards: the inbox is linked only when status is active and isMailboxSellerCredentialType(credentialType) is true. Your browser origin needs access to Peer's API to read status; otherwise read it from your server.
Hosted X Money receipt linking
openXMoneyConnect() and getXMoneyConnectUrl() (SDK 0.14.10 or later) open the same page for X Money and take the same options. payeeUsername is required: the trimmed, lowercased X username without @ must hash to payeeDetails, or the helper throws ValidationError. Failures reject with XMoneyConnectError. The seller needs X Money email notifications on and a received payment in the inbox.
import { openXMoneyConnect } from "@zkp2p/sdk";
// Call directly from the link button's click handler.
await openXMoneyConnect({ payeeDetails, payeeUsername: "yourhandle" });
Cash-out products can use Peer Cash's X Money helpers.
uploadGoogleOAuthSellerCredential()
uploadGoogleOAuthSellerCredential(params, opts?) sends a one-time Google authorization code for a Gmail inbox to Peer's API, which exchanges it and stores the credential. The code must come from Peer's Google OAuth client and a redirect URI registered for it, so partner apps should use the hosted page.
| Field | Required | Description |
|---|---|---|
platform | Yes | paypal, venmo, or xmoney |
payeeDetails | Yes | Registered payee hash |
authorizationCode | Yes | One-time Google OAuth code |
redirectUri | Yes | Exact redirect URI from the authorization request |
payeeEmail | PayPal only | PayPal receiving email |
uploadMicrosoftOAuthSellerCredential() connects a personal Outlook inbox. It takes the same fields plus codeVerifier from the PKCE request. Both accept opts with baseApiUrl, timeoutMs, and signal.
Codes are single-use and the SDK doesn't retry. If the response is lost, start consent again. For Venmo, Peer's API resolves the account from the registered username, so don't send an account ID. For X Money, it uses the registered X username; don't send payeeEmail. XMONEY_EMAIL_RECEIPT_NOT_FOUND means the inbox has no received-payment email for that account: the seller should turn on X Money email notifications, receive a payment, and reconnect.
uploadICloudMailSellerCredential()
Connects an iCloud Mail inbox for PayPal or Venmo (SDK 0.14.7 or later) or X Money (0.14.9 or later). iCloud has no OAuth, so the seller creates an Apple app-specific password and your app passes it with the payee hash. Returns the stored status, { platform, payeeIdHash, status: "active", credentialType: "icloud_mail" }.
| Field | Required | Description |
|---|---|---|
platform | Yes | paypal, venmo, or xmoney |
payeeDetails | Yes | Registered payee hash |
mailboxEmail | Yes | Primary iCloud Mail address that receives the receipts. Trimmed and lowercased |
appSpecificPassword | Yes | Apple app-specific password. Encrypted to the enclave; never retained |
payeeEmail | PayPal only | PayPal receiving email. keccak256 of the lowercased email must equal payeeDetails |
payeeUsername | Venmo, X Money | Trimmed, with a leading @ removed, and lowercased. X Money: the X username, whose hash must equal payeeDetails. Venmo: the seller's Venmo username; its account ID's hash must equal payeeDetails |
| Option | Description |
|---|---|
baseApiUrl | API root override |
attestationServiceUrl | Defaults to the attestor that matches baseApiUrl. Must be one of Peer's attestors; other hosts throw before any request |
attestationRuntime | fetch, subtle, or getRandomValues overrides for runtimes without WebCrypto, such as React Native |
The method runs these steps once. Apart from the proxy fallback in step 2, it doesn't retry.
- Payee check. PayPal and X Money hashes are checked locally. For Venmo, Peer's API looks up the account ID for
payeeUsernamefrom Venmo's public profile, and the SDK checks its hash. A mismatch rejects before any mailbox work. - Upload. The SDK verifies the enclave's attestation, encrypts the password to it, and sends it to the attestor. After a network failure, it retries once through that attestor's proxy, which serves the same enclave. The enclave signs in to the inbox and requires a received-payment receipt for this payee.
- Bundle check. The SDK verifies the returned bundle against the enclave's pinned signer, the platform, and
payeeDetails, and requirescredentialType: "icloud_mail". - Store. Peer's API stores the unchanged bundle. The payee isn't registered again.
Only one connection per inbox runs at a time in a process. Failures reject with ICloudMailCredentialError; route on its reason:
reason | Meaning |
|---|---|
payee_mismatch | The payee doesn't hash to payeeDetails; no mailbox work ran |
payee_not_found | Venmo has no account for that username; no mailbox work ran |
maker_not_found | Peer's API has no payee with this hash |
mailbox_busy | Another connection for this inbox is running |
rate_limited | Try again after retryAfterMs |
receipt_not_found | No verified received-payment receipt for this payee in the inbox |
app_password_rejected | Apple rejected the app-specific password; the seller creates a new one |
upload_rejected | The enclave rejected the upload for another reason; see cause |
unavailable | Temporary attestor, API, or network failure; a user-triggered retry may succeed |
bundle_unverified | The attestation, signature, payee binding, or credential type failed verification; nothing was stored |
auth_required | Peer's API rejected the request's authorization |
store_failed | Peer's API didn't store an active iCloud credential for this payee |
An empty mailboxEmail or appSpecificPassword, a missing payeeEmail or payeeUsername, or an empty Venmo payeeUsername throws ValidationError.
import { ICloudMailCredentialError } from "@zkp2p/sdk";
try {
const status = await client.uploadICloudMailSellerCredential({
platform: "venmo",
payeeUsername: "your-venmo-username",
payeeDetails: registeredPayeeHash,
mailboxEmail: "seller@icloud.com",
appSpecificPassword,
});
console.log(status.credentialType); // "icloud_mail"
} catch (error) {
if (!(error instanceof ICloudMailCredentialError)) throw error;
// Show copy for error.reason; error.retryAfterMs is set for rate_limited.
}
React Native has no WebCrypto by default. Pass { attestationRuntime: { subtle, getRandomValues } } as the second argument, plus fetch if your runtime needs one.
isMailboxSellerCredentialType()
isMailboxSellerCredentialType(credentialType) (SDK 0.14.7 or later) returns true for google_oauth (Gmail), microsoft_oauth (Outlook), and icloud_mail (iCloud Mail). It accepts string | null | undefined.
import { isMailboxSellerCredentialType } from "@zkp2p/sdk";
const { responseObject } = await client.getSellerCredentialStatus({
processorName: "venmo",
payeeDetails,
});
const linked =
responseObject.status === "active" &&
isMailboxSellerCredentialType(responseObject.credentialType);
getSellerCredentialStatus()
getSellerCredentialStatus({ processorName, payeeDetails }, opts?) reads a payee's public credential status. responseObject is { platform, payeeIdHash, status, credentialType }, where status is active, inactive, or missing. opts takes baseApiUrl and timeoutMs.
const response = await client.getSellerCredentialStatus(
{
processorName: "paypal",
payeeDetails:
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
},
{ timeoutMs: 10_000 },
);
Standalone API and attestation helpers
Low-level helpers for calling Peer's API or the attestation service without a client. Pass baseApiUrl as the API root, https://api.zkp2p.xyz.
| Helper | Purpose |
|---|---|
apiGetQuote(req, { baseApiUrl, timeoutMs? }), apiGetQuotesBestByPlatform(req, { baseApiUrl, timeoutMs? }) | Quote calls |
apiSignIntentV3(request, { baseApiUrl, timeoutMs? }) | Fetch a gating signature for a signal. Returns { signature, signatureExpiration, ... } |
apiGetOrderbook(params, { baseApiUrl, timeoutMs? }) | Orderbook entries for a currency |
apiGetOrderbookTable(params, { baseApiUrl, timeoutMs?, runtimeEnv? }) | Aggregated orderbook rows. With runtimeEnv, drops rows on escrows signalIntent() can't use |
apiGetDepositBundle({ depositId, escrowAddress, dailySnapshotLimit? }, { baseApiUrl, timeoutMs? }) | One deposit with its intents, events, profit snapshots, fund activities, and daily snapshots |
apiValidatePayeeDetails(req, baseApiUrl, timeoutMs?) | Validate a payee before registration |
apiPostDepositDetails(req, baseApiUrl, timeoutMs?) | Register one payee; returns its hashedOnchainId |
apiGetSellerCredentialStatus(processorName, payeeDetails, baseApiUrl, timeoutMs?) | Seller credential status |
apiUploadSellerCredentialBundle(params, baseApiUrl?, timeoutMs?) | Register the payee if needed, check the bundle's payee hash, and store the bundle |
apiUploadGoogleOAuthSellerCredential(...), apiUploadMicrosoftOAuthSellerCredential(...) | Inbox code uploads behind the client methods |
apiGetReferralDashboard({ baseApiUrl, address }), apiGetReferralEarnings({ baseApiUrl, address }) | Public referral reads |
apiLookupReferralCode(code, { baseApiUrl }) | Resolve a referral code |
apiCreateReferralCode, apiRedeemReferralCode, apiUpdateReferralCode | Referral writes; send the signed body the client's WithSignature methods build |
createNitroAttestationClient(opts) | Verify the Nitro enclave and request identity attestations, Buyer TEE attestations, or seller credential bundles |
apiRequestIdentityAttestation(payload, attestationServiceUrl, platform, actionType, options?) | Request an identity attestation with already-encrypted session material |
createEncryptedBuyerTeeSessionMaterial(input) | Encrypt Buyer TEE session material for a payment proof |
apiVerifyBuyerTeePayment(payload, attestationServiceUrl, platform, actionType, options?) | Request a Buyer TEE payment attestation |
apiCreateSellerCredentialBundle(payload, attestationServiceUrl, platform, timeoutMs?, runtime?, options?) | Create a signed seller credential bundle |
getAmazonPaySellerIdentity(bundle) | The receiving UPI ID inside an Amazon Pay bundle |
apiGetOrderbook() and apiGetOrderbookTable() take { currency, paymentPlatform?, takerAddress?, sortBy?, sortDirection?, sellerAutomatedRelease?, limit?, offset?, amount?, showSmallOrders?, hideExtremeSpread?, chainId?, token? }. sortBy is price, available, limits, or apr. sellerAutomatedRelease is include, exclude, or only.
Querying on-chain data
These reads go to the chain through ProtocolViewer, so they don't lag behind the indexer:
- Deposits:
getDeposits()(the connected wallet's),getAccountDeposits(owner),getDeposit(depositId),getDepositsById(ids) - Intents:
getIntents()(the connected wallet's),getAccountIntents(owner),getIntent(intentHash) - Raw ProtocolViewer views:
getPvDepositById(depositId),getPvDepositsFromIds(ids),getPvAccountDeposits(owner),getPvAccountIntents(owner),getPvIntent(intentHash) - Other:
resolvePayeeHash(depositId, paymentMethodHash),getFulfillIntentInputs(intentHash),getDepositPreIntentHook(depositId, options?),getDepositWhitelistHook(depositId, options?),getDeployedAddresses(),getUsdcAddress()
Intent reads cover OrchestratorV2, V3, and V4 intents. For examples, see Offramp Integration.
Indexer
Use client.indexer for history, filtering, and pagination across all deposits and intents. The production endpoint is https://indexer.zkp2p.xyz/v1/graphql. It is rate limited per caller; the client retries 429 responses after Retry-After. Indexed data can trail a confirmed transaction, so confirm with the receipt and on-chain reads.
Indexer deposit IDs have the form escrowAddress_depositId, with the escrow address in lowercase. Build one with createCompositeDepositId(escrowAddress, depositId).
const deposits = await client.indexer.getDeposits(
{ status: "ACTIVE", depositor: "0xMakerAddress" },
{ limit: 25, orderBy: "updatedAt", orderDirection: "desc" },
);
Deposit filters accept status (ACTIVE or CLOSED), depositor, delegate, delegateIsSet, chainId, escrowAddress, escrowAddresses, minLiquidity, and acceptingIntents. Pagination accepts limit, offset, orderBy, and orderDirection.
Deposit queries
getDeposits(filter?, pagination?)getDepositsWithRelations(filter?, pagination?, options?)getDepositById(compositeId, options?)getDepositsByIds(ids)getDepositsByIdsWithRelations(ids, options?)getDepositsByPayeeHash(payeeHash, options?)
Intent queries
getIntentsForDeposits(depositIds, statuses?)getOwnerIntents(owner, statuses?)getIntentsByRateManager(rateManagerId, statuses?)getIntentByHash(intentHash)getExpiredIntents({ now, depositIds, limit? })getFulfilledIntentEvents(intentHashes): fulfillment eventsgetIntentFulfillmentAmounts(intentHash): includestakerAmountNetFees, the net USDC the taker receivedgetFulfillmentAndPayment(intentHash)
Fund activity and snapshots
getDepositFundActivities(depositId)getMakerFundActivities(depositor, limit?)getDepositDailySnapshots(depositId, limit?)getProfitSnapshotsByDeposits(depositIds)
Rate manager (vault) queries
getRateManagers(pagination?, filter?)getRateManagerDetail(managerId, options?)getRateManagerDelegations(managerId, pagination?)getDelegationForDeposit(depositId, options?)getManagerDailySnapshots(managerId, options?)getManualRateUpdates(managerId, options?)getOracleConfigUpdates(managerId, options?)
Staking queries
getStakingState({ chainId, environment, vaultAddress, disputeProtectionPolicyAddress, taker, stakeOwner })getStakeTotals({ chainId, vaultAddress }):totalStaked,totalLocked, andstakerCountgetStakeLocks({ chainId, vaultAddress, stakeOwner, status?, limit?, cursor? })getTakerStakeAuthorizations({ chainId, vaultAddress, stakeOwner | taker, authorized?, limit?, cursor? }): the takers an owner authorized, or the owners that authorized a takergetStakeActivity({ chainId, vaultAddress, merchant?, kind?, orderDirection?, limit?, cursor? }):merchantmatches the stake owner or taker; omit it for the whole vault.orderDirectiondefaults toascgetDisputeProtectionIntentByHash({ chainId, intentHash }): the indexed protection, including released and disputed ones, ornullgetDisputeProtectionIntents({ chainId, policyAddress, throughBlockNumber, limit?, cursor? })
Page methods return { items, nextCursor, asOf }. Pass cursors back unchanged, and keep orderDirection the same across pages. Limits are integers from 1 to 100, and numbers are unsigned decimal strings. asOf is the latest row in the page, not an indexer head or consistency watermark.
getDisputeProtectionIntents needs throughBlockNumber, an inclusive upper bound on disputedAtBlockNumber; use one finalized block for every page of a pass. Its page also returns endCursor, the cursor of its last item (null only for an empty page). Follow nextCursor within a pass, then store the final endCursor for the next poll.
Raw access
query<T>({ query, variables? }): raw GraphQLclient: the underlyingIndexerClient
The package also exports IndexerClient, IndexerDepositService, IndexerRateManagerService, IndexerStakingService, and fetchIndexerFulfillmentAndPayment(client.indexer.client, intentHash).
Indexer converters
These turn indexer payloads into the EscrowDepositView shape that RPC reads return.
| Helper | Purpose |
|---|---|
convertIndexerDepositToEscrowView(deposit, chainId, escrowAddress) | Convert one indexer deposit, with relations |
convertDepositsForLiquidity(deposits, chainId, escrowAddress, options?) | Keep active, funded deposits that accept intents. Pass { includePrivateOrderbooks: true } to keep deposits behind a whitelist hook |
convertIndexerIntentsToEscrowViews(intents, depositViewsById) | Convert indexer intents to EscrowIntentView[] |
Oracle helpers
With an oracle config, a deposit's floor rate for a currency is the greater of its minimum rate and the Chainlink rate after the spread. If the feed is stale or invalid, that currency stops accepting intents. The config is { adapter, adapterConfig, spreadBps, maxStaleness }. Take adapter, adapterConfig, and maxStaleness from getSpreadOracleConfig(currency). spreadBps is a signed spread on the oracle rate; -50 is 0.5% below it.
import {
getSpreadOracleConfig,
resolveFiatCurrencyBytes32,
resolvePaymentMethodHash,
} from "@zkp2p/sdk";
const oracle = getSpreadOracleConfig("USD");
if (!oracle) throw new Error("No bundled oracle config for USD");
await client.setOracleRateConfig({
depositId: 42n,
paymentMethodHash: resolvePaymentMethodHash("wise"),
currencyHash: resolveFiatCurrencyBytes32("USD"),
config: {
adapter: oracle.adapter,
adapterConfig: oracle.adapterConfig,
spreadBps: -50,
maxStaleness: oracle.maxStaleness,
},
});
| Helper | Purpose |
|---|---|
getSpreadOracleConfig(currency, adapters?) | The bundled Chainlink oracle config for a fiat currency, or null |
encodeSpreadOracleAdapterConfig(config) | Encode a Chainlink adapter config |
validateOracleFeedsOnChain(publicClient) | The currencies whose bundled feeds are available on-chain |
supportsInlineOracleRateConfig({ escrowAddress? }) | Client method: whether the target escrow accepts inline oracle configs |
Constants: CHAINLINK_ORACLE_ADAPTER, DEFAULT_ORACLE_MAX_STALENESS_SECONDS, CHAINLINK_ORACLE_FEEDS, and its deprecated alias SPREAD_ORACLE_FEEDS. PYTH_ORACLE_ADAPTER, PYTH_ORACLE_FEEDS, and encodePythAdapterConfig() remain only for compatibility; the bundled Pyth feed map is empty and isn't a fallback.
Referrer fees
Helpers to validate referrer fee settings before getQuote() or signalIntent().
| Helper | Purpose |
|---|---|
assertValidReferrerFeeConfig(config, context) | Throws if the config is invalid for getQuote, getQuotesBestByPlatform, or signalIntent |
isValidReferrerFeeRecipient(value) | Whether the recipient is a valid address |
isValidReferrerFeeBps(value) | Whether the value is an integer from 0 to 10000 |
parseReferrerFeeConfig(recipient, feeBpsValue) | Build a ReferrerFeeConfig from loose input, or null |
referrerFeeConfigToPreciseUnits(config) | Convert feeBps to the 18-decimal fee used on-chain |
Attribution
The SDK appends ERC-8021 builder codes to transaction calldata. Pass your own codes in txOverrides.referrer; the SDK adds BASE_BUILDER_CODE after them.
await client.signalIntent({
...params,
txOverrides: { referrer: ["my-app", "campaign-42"] },
});
createDeposit() and prepareCreateDeposit() also take referralCode, a Peer referral code for deposit attribution. It must be six letters or digits; the SDK trims and uppercases it, then adds the code peer-ref- plus the referral code before your other codes. It doesn't set a fee or redeem a referral. A peer-ref- code already in txOverrides.referrer throws, so send a referral through one option only.
| Helper | Purpose |
|---|---|
getAttributionDataSuffix(referrer?) | Build the attribution suffix |
appendAttributionToCalldata(calldata, referrer?) | Append attribution to existing calldata |
encodeWithAttribution(request, referrer?) | Encode calldata and append attribution |
sendTransactionWithAttribution(walletClient, request, referrer?, overrides?) | Send a transaction with attribution |
Constants: BASE_BUILDER_CODE, ZKP2P_IOS_REFERRER, ZKP2P_ANDROID_REFERRER.
Contract helpers
| Helper | Description |
|---|---|
getContracts(chainId, env?) | Deployed addresses and ABIs for escrow, orchestrators, verifiers, ProtocolViewer, USDC, and others |
getRateManagerContracts(chainId, env?) | Rate-manager registry and controller |
getStakeVaultContract(chainId, env?) | StakeVault address, ABI, and stake token |
getDisputeProtectionPolicyContract(chainId, env?) | DisputeProtectionPolicy address and ABI |
getOrchestratorV3Contract(chainId, env?) | OrchestratorV3 address and ABI |
getOrchestratorV4Contract(chainId, env?), getDisputeProtectionPolicyV2Contract(chainId, env?), getStakeVaultV2Contract(chainId, env?) | OrchestratorV4 stack contracts; throw where not deployed |
getIntentGuardianContract(chainId, env?), hasIntentGuardian(chainId, env?) | IntentGuardian address and ABI, and whether one is deployed |
getPaymentMethodsCatalog(chainId, env?) | Platform-to-hash catalog for the deployment |
getGatingServiceAddress(chainId, env?) | Gating-service signer for OrchestratorV3 deposits |
A client also has getStakeVaultContract(), getOrchestratorV3Contract(), and getDisputeProtectionPolicyContract(), resolved for its chain and environment.
Currency and platform helpers: Currency, currencyInfo, getCurrencyInfoFromHash(), getCurrencyInfoFromCountryCode(), resolveFiatCurrencyBytes32(), resolvePaymentMethodHash(), resolvePaymentMethodHashFromCatalog(), and resolvePaymentMethodNameFromHash().
Error handling
SDK errors extend ZKP2PError:
| Class | Code | Extra fields | Use it for |
|---|---|---|---|
ZKP2PError | Any ErrorCode | details?, field? | Shared base class |
ValidationError | VALIDATION | field?, details? | Invalid input |
NetworkError | NETWORK | details? | RPC or network failures |
APIError | API | status?, details? | Failed API requests |
ContractError | CONTRACT | details? | Contract call or simulation failures |
ErrorCode values: VALIDATION, NETWORK, API, CONTRACT, UNKNOWN. These errors extend Error instead: AccessPolicyUnsupportedError, ICloudMailCredentialError, VenmoGmailConnectError, and XMoneyConnectError. Wallet and transport failures can also surface as plain Error.
import {
APIError,
ContractError,
ValidationError,
ZKP2PError,
} from "@zkp2p/sdk";
try {
await client.createDeposit({
/* ... */
});
} catch (error) {
if (error instanceof ValidationError) {
console.error(error.field, error.message);
} else if (error instanceof APIError) {
console.error(error.status, error.message);
} else if (error instanceof ContractError) {
console.error(error.details);
} else if (error instanceof ZKP2PError) {
console.error(error.code, error.message);
}
}
Logging
import { setLogLevel } from "@zkp2p/sdk";
setLogLevel("debug"); // 'debug' | 'info' | 'error'; defaults to 'info'
Help?
For integration questions, join the Peer Telegram community. For order-specific help, email support@peer.xyz.