Skip to main content

Build Your Own Extension

The Peer extension is a thin capture bridge. It opens the payment platform in a tab, intercepts requests that match a provider template, extracts payment rows, encrypts session material for the attestation service, and posts the result to the page. Your app does everything else, including quotes, intents, fulfillment and seller registration, through @zkp2p/sdk.

You can replace the bridge with an extension under your own name, icon, Web Store listing and consent UI. If the Peer extension works for you as is, follow Onramp Integration instead.

Start from the open-source branded extension template: fork it, edit brand.config.json, run npm run rebrand, and keep the security requirements.

Architecture​

ComponentResponsibility
Your extensionOpens the platform tab, intercepts template-matched requests, extracts rows, encrypts session material, and posts results to the page
Your web app + @zkp2p/sdkQuotes, signalIntent(), fulfillIntent(), seller registration and credential upload
Peer attestation serviceDecrypts session material inside a TEE, re-verifies the payment or credential with the platform, and signs the result

Plaintext session material (cookies, auth headers, request bodies) never leaves the extension. The extension encrypts it to the attestation service and returns only the encrypted blob and non-sensitive metadata rows.

The page contract​

The Peer extension injects window.peer. If your extension implements the same interface, page code, including createPeerExtensionSdk() from @zkp2p/sdk, works unchanged:

interface Peer {
getVersion(): Promise<string>;
requestConnection(): Promise<boolean>;
checkConnectionStatus(): Promise<"connected" | "disconnected" | "pending">;
authenticate(params: PeerAuthenticateParams): void;
onMetadataMessage(
callback: (message: PeerMetadataMessage) => void,
): () => void;
}

authenticate() starts a capture:

ParameterDescription
actionTypeProvider template action, such as transfer_venmo
platformPlatform the template belongs to, such as venmo
captureModebuyerTee for buyer payments and identity registration, sellerCredential for Seller Autopilot
attestationServiceUrlRequired for buyerTee. Optional for sellerCredential, where it defaults to the production service
attestationActionType / attestationPlatformAttestation route, when it differs from the template's actionType / platform
callerAddressSeller wallet address. Required for Amazon Pay UPI seller capture

The result arrives as a PeerMetadataMessage with requestId, platform, expiresAt, the extracted metadata rows, and either errorMessage or one of:

  • buyerTeeCapture: { encryptedSessionMaterial, params? } for buyer and identity captures
  • sarCredentialCapture: { credentialBundle, offchainId } for Seller Autopilot

See Metadata Row Selection for the row shape.

Provider templates​

Templates tell the extension what to intercept and how to extract rows. Peer's API serves them at a fixed path:

https://api.zkp2p.xyz/providers/{platform}/{actionType}.json

For example, https://api.zkp2p.xyz/providers/venmo/transfer_venmo.json. The Peer extension loads every template from this path and accepts no template or template URL from the page. Your extension should do the same.

Template shape​

{
"authLink": "https://provider.example/login",
"url": "https://provider.example/api/replay",
"method": "GET",
"body": "",
"metadata": {
"platform": "provider",
"method": "GET",
"urlRegex": "https://provider.example/api/transactions.*",
"bodyRegex": "",
"fallbackMethod": "GET",
"fallbackUrlRegex": "",
"fallbackBodyRegex": "",
"metadataUrl": "",
"metadataUrlMethod": "GET",
"metadataUrlBody": "",
"preprocessRegex": "",
"transactionsExtraction": {
"transactionJsonPathListSelector": "$.transactions",
"transactionJsonPathSelectors": {
"paymentId": "$.id",
"amount": "$.amount.value",
"currency": "$.amount.currency",
"date": "$.created_at",
"recipient": "$.recipient.name"
}
}
},
"paramNames": [],
"paramSelectors": []
}
FieldPurpose
authLinkPage opened in the capture tab
metadata.platformPlatform name returned in the capture result
metadata.method / urlRegex / bodyRegexPrimary request to intercept
metadata.fallbackMethod / fallbackUrlRegex / fallbackBodyRegexFallback request to intercept
url / method / bodyRequest replayed with the fallback request's headers when only the fallback matched
metadata.metadataUrl / metadataUrlMethod / metadataUrlBodyRequest replayed with the matched request's headers, instead of reading that request's response
metadata.shouldReplayRequestInPageReplay inside the provider tab instead of the extension
metadata.preprocessRegexRegex whose capture group 1 is parsed before extraction
metadata.transactionsExtractionJSONPath or XPath selectors that produce metadata rows
metadata.userInputClick guide for platforms where the user must open a transaction first
metadata.shouldSkipCloseTabKeep the provider tab open after a successful capture
paramNames / paramSelectorsPublic attestation params, see Buyer TEE params
captureJsEnabled / captureJsWhen true, the template ships sandboxed JavaScript capture code instead of selectors

MV3 webRequest doesn't expose response bodies, so the Peer extension replays each matched request with its captured headers to read the response. Some templates set captureJsEnabled: true; the Peer extension runs their captureJs source in a sandbox. If your extension can't, leave those platforms out.

Metadata extraction​

For JSON responses, use a list selector plus field selectors relative to each row:

{
"metadata": {
"transactionsExtraction": {
"transactionJsonPathListSelector": "$.activity_rows",
"transactionJsonPathSelectors": {
"paymentId": "$.id",
"amount": "$.amount.value",
"currency": "$.amount.currency",
"date": "$.created_at"
}
}
}
}

Omit transactionJsonPathListSelector to run the selectors against the root object and return one row. For HTML responses, use transactionXPathListSelector and transactionXPathSelectors. Each row carries the selected fields plus originalIndex and hidden; a row with a missing field is hidden.

When the user must click an element first, such as a transaction's detail view, the template includes metadata.userInput. transactionXpath is required:

{
"metadata": {
"userInput": {
"promptText": "Select the transaction to share.",
"transactionXpath": "//button[contains(., 'Details')]",
"waitForXpathMs": 8000,
"pollIntervalMs": 250
}
}
}

Buyer TEE params​

paramNames and paramSelectors define the public params extracted for each row, alongside the encrypted session material:

{
"paramNames": ["PAYMENT_ID", "ACCOUNT_ID"],
"paramSelectors": [
{ "type": "jsonPath", "value": "$[{{INDEX}}].id" },
{ "type": "regex", "source": "url", "value": "account/([^/?]+)" }
]
}
  • type: jsonPath, regex, or xPath.
  • source: responseBody (default), requestBody, or url. Reject header selectors.
  • {{INDEX}} is replaced with the row's originalIndex.
  • requestBody selectors are private. Never copy their values into the rows returned to the page.

Identity registration templates​

Identity templates such as register_paypal, register_business_paypal, register_wise and register_alipay use the same shape and the buyerTee capture mode. The page sends the capture to the attestation service's identity endpoint instead of fulfillIntent(); see Identity attestation.

Develop a new platform​

To add capture for a new payment platform, use the Provider builder. It guides you through capture, encrypted test replay and sharing. Pages can't pass templates to the Peer extension, so test against a published template or through the Provider builder. To get a provider published, ask on Telegram.

Implementing the buyer flow​

The buyer flow turns a payment the user already made into encrypted session material for fulfillIntent(). It is template-driven: a new buyer platform needs a template, not extension code.

  1. Launch. The page calls authenticate({ actionType, platform, captureMode: 'buyerTee', attestationServiceUrl }). Load the template and open authLink in a new tab.
  2. Intercept. Register chrome.webRequest listeners (onBeforeRequest, onSendHeaders, onResponseStarted) for urlRegex, fallbackUrlRegex and metadataUrl. Cache the matched request's URL, method, headers and body, then replay it to read the response.
  3. Extract. Run transactionsExtraction over the response to build rows, and paramSelectors to build each row's params. The Peer extension uses jsonpath-plus.
  4. Encrypt. Build session material from the captured request and encrypt it in the extension:
import { createEncryptedBuyerTeeSessionMaterial } from "@zkp2p/sdk";

const sessionMaterial: Record<string, string> = {
...capturedRequestHeaders, // header name -> value
...(capturedRequestBody ? { body: capturedRequestBody } : {}),
url: capturedRequestUrl,
};

const encryptedSessionMaterial = await createEncryptedBuyerTeeSessionMaterial({
platform: "venmo",
actionType: "transfer_venmo",
attestationServiceUrl, // from the launch payload
sessionMaterial,
});
  1. Return. Post { requestId, platform, metadata, expiresAt, buyerTeeCapture: { encryptedSessionMaterial, params } } to the document that launched the capture, then discard all capture state.

The page then builds { proofType: 'buyerTee', encryptedSessionMaterial, params } from the selected row and calls fulfillIntent(), as described in Onramp Integration, including the per-platform params.index rule.

MV3 service workers have no DOM, so run XPath extraction in an offscreen document. When you call the SDK encryption helpers outside a window, pass attestationRuntime: { fetch, subtle, getRandomValues } (type SellerCredentialAttestationRuntime).

Implementing the Seller Autopilot flow​

Seller Autopilot lets a seller upload encrypted platform credentials once, so incoming payments verify and release automatically. Extension capture covers Cash App and Amazon Pay UPI. Venmo, PayPal and X Money sellers connect a receipt inbox, and Wise sellers use a personal API token; neither needs the extension. See Seller Autopilot for those flows.

The Seller Autopilot result contains only the encrypted credentialBundle and the seller's offchainId. Never post captured headers, cookies, payee IDs or session material to the page.

  1. Launch. The page calls authenticate() with captureMode: 'sellerCredential':
    • Cash App: actionType: 'transfer_cashapp', platform: 'cashapp'
    • Amazon Pay UPI: actionType: 'transfer_upi_amazonpay', platform: 'upi', plus the seller's callerAddress
  2. Capture. Use the same template-driven interception as the buyer flow.
  3. Parse. Build the platform's upload payload from the captured request, in memory. This is the only platform-specific code in the extension:
PlatformPayloadoffchainId
Cash App{ payeeId: cashtag, sessionMaterial: { recipientCashtag, customerId, sessionCookie, requestPayload } }The cashtag
Amazon Pay UPI{ callerAddress, sessionMaterial: { sessionCookie, userAgent } }, with no payeeIdgetAmazonPaySellerIdentity(credentialBundle)
  1. Bundle. Create the encrypted credential bundle through the attestation service:
import {
apiCreateSellerCredentialBundle,
type SellerCredentialAttestationRuntime,
} from "@zkp2p/sdk";

const attestationRuntime: SellerCredentialAttestationRuntime = {
fetch: globalThis.fetch.bind(globalThis),
subtle: globalThis.crypto.subtle,
getRandomValues: globalThis.crypto.getRandomValues.bind(globalThis.crypto),
};

const response = await apiCreateSellerCredentialBundle(
payload, // from step 3
attestationServiceUrl,
platform,
undefined, // timeoutMs
attestationRuntime,
);

const credentialBundle = response.responseObject;
  1. Return. Post { requestId, platform, metadata: [], expiresAt, sarCredentialCapture: { credentialBundle, offchainId } } to the page and discard everything else.

The page finishes registration. Your extension must not call Peer's API or create makers:

await client.uploadSellerCredentialBundle({
platform: "cashapp",
offchainId: capture.offchainId,
bundle: capture.credentialBundle,
});

// Then read status with client.getSellerCredentialStatus().

uploadSellerCredentialBundle() registers or recovers the payee through Peer's API (POST /v2/makers/create), checks that the returned hashedOnchainId equals bundle.payeeIdHash, then stores the bundle (POST /v2/makers/{platform}/{hashedOnchainId}/seller-credential). Keep that hash check if you make these calls yourself: it stops a tampered capture from binding credentials to the wrong payee.

SDK reference for extension builders​

ExportFlowUse
createEncryptedBuyerTeeSessionMaterial({ platform, actionType, attestationServiceUrl, sessionMaterial })Buyer, identityEncrypt captured session material to the attestation service
createNitroAttestationClient({ environment, attestationServiceUrl })IdentityRequest typed identity attestations
apiRequestIdentityAttestation(payload, attestationServiceUrl, platform, actionType)IdentityPost encrypted identity session material to the attestation service
apiCreateSellerCredentialBundle(payload, attestationServiceUrl, platform, timeoutMs?, runtime?)Seller AutopilotCreate the encrypted seller credential bundle
getAmazonPaySellerIdentity(bundle)Seller AutopilotRead the UPI ID the attestation service derived for an Amazon Pay bundle
apiUploadSellerCredentialBundle(params, baseApiUrl?, timeoutMs?)Seller AutopilotPage-side: register or recover the payee, check bundle.payeeIdHash, store the bundle
SellerCredentialAttestationRuntimeBothInject fetch, subtle and getRandomValues in extension runtimes
PeerAuthenticateParams, PeerMetadataMessage, PeerMetadataRow, SellerCredentialBundleBothTypes for the page contract

Page-side calls (quotes, intents, fulfillment, credential uploads) are covered in the Client Reference.

Security requirements​

  • Keep plaintext in the extension. Encrypt session material in the extension, post only the encrypted blob and extracted rows, and discard the plaintext.
  • Encrypt only to Peer's attestation service. The SDK helpers verify the service's Nitro attestation against bundled PCR pins. Don't accept an arbitrary attestation URL from the page.
  • Return only the bundle for Seller Autopilot. The result holds credentialBundle and offchainId, nothing else.
  • Keep requestBody selector values private. Never copy them into returned rows.
  • Approve each third-party origin. Gate captures behind per-origin connection approval, as requestConnection() does, and return results only to the requesting document and frame. You may pre-approve your own domains and localhost.
  • Keep capture state in memory. Scope capture sessions to the tab and clear them when the capture completes, fails or the tab closes. Persist only the user's connection approvals.
  • Request only the hosts you need. Limit host permissions to the payment platforms you support.
  • Keep maker logic in the page. The extension never holds API keys or calls Peer's API.

For help with templates or attestation service expectations, ask on Telegram.