Documentation
XyncPay Developer Docs
Integrate non-custodial protocol translation for AI agent payments. Translate between x402, MPP, and AP2 with a single API.
Getting Started
Quickstart
Set up XyncPay and process your first cross-protocol payment in minutes. XyncPay is a REST API. You only need ethers for wallet signing.
1. Install Dependencies
XyncPay uses the REST API directly. Install ethers for wallet signing.
npm install ethers2. Configure Environment
Set the API base URL and your wallet private key. These values are read by your local Node.js process only. ethers uses the private key to sign requests on your machine; XyncPay receives only the resulting signature.
Non-custodial
Your private key stays on your machine and is read only by your local Node.js process. ethers signs each request locally; only the resulting signature is transmitted to XyncPay. XyncPay never receives, stores, or has access to your private key. This is the same model used by MetaMask and every non-custodial web3 application.
# .env.local
XYNCPAY_API_URL=https://www.xyncpay.com
WALLET_ADDRESS=0xYourWalletAddress
PRIVATE_KEY=0xYourPrivateKey3. Register Your Agent
Registration uses a two-step challenge-response to verify wallet ownership. Step 1 requests a challenge string. Step 2 signs it and creates the agent record.
import { Wallet } from "ethers";
const API = process.env.XYNCPAY_API_URL;
const wallet = new Wallet(process.env.PRIVATE_KEY);
// Step 1: request a challenge
const r1 = await fetch(`${API}/api/v1/agents/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
walletAddress: wallet.address,
preferredChain: "base",
}),
});
const { data: { challenge, nonce } } = await r1.json();
// Step 2: sign the challenge and complete registration
const signature = await wallet.signMessage(challenge);
const r2 = await fetch(`${API}/api/v1/agents/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
walletAddress: wallet.address,
preferredChain: "base",
signature,
nonce,
name: "my-agent",
supportedProtocols: ["x402", "mpp"],
supportedChains: ["base"],
}),
});
const { data: agent } = await r2.json();
console.log("xyncId:", agent.xyncId); // e.g. xync_6b8dd882f7a94bcb4. Create a Session
Sessions enforce spending limits. Create one before making payments. Sign the request body with your wallet before sending.
const sessionBody = JSON.stringify({
agentId: agent.xyncId,
spendingCap: "10000000", // 10 USDC (6 decimals)
perTransactionLimit: "5000000", // 5 USDC per transaction
allowedChains: ["base"],
allowedCurrencies: ["USDC"],
expiresInSeconds: 3600,
});
const sessionRes = await fetch(`${API}/api/v1/sessions/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Wallet-Address": wallet.address,
"X-Wallet-Signature": await wallet.signMessage(sessionBody),
},
body: sessionBody,
});
const { data: session } = await sessionRes.json();
console.log("Session:", session.sessionId); // e.g. sess_488b8111cdaa4539a5be5. Translate a Payment
Send a translation request to convert between protocols. The API returns an unsigned transaction. Your agent signs it locally before broadcasting.
const body = JSON.stringify({
sourceProtocol: "x402",
targetProtocol: "mpp",
payeeAddress: "0xRecipientAddress",
amount: "1000000",
currency: "USDC",
chain: "base",
sessionId: session.sessionId,
});
const response = await fetch(`${API}/api/v1/payments/translate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Wallet-Address": wallet.address,
"X-Wallet-Signature": await wallet.signMessage(body),
},
body,
});
const { data: payment } = await response.json();
console.log("Fee:", payment.request.feeAmount, payment.request.currency);
console.log("Payment ID:", payment.paymentId);6. Sign & Submit
Sign the returned unsigned transaction with your wallet and submit it to Base. XyncPay never touches your private key.
import { Wallet, JsonRpcProvider } from "ethers";
const provider = new JsonRpcProvider("https://mainnet.base.org");
const signer = new Wallet(process.env.PRIVATE_KEY, provider);
const signedTx = await signer.signTransaction(payment.unsignedTransaction);
const txResponse = await provider.broadcastTransaction(signedTx);
const receipt = await txResponse.wait();
console.log("Settled:", receipt.hash);7. Verify On-Chain
Confirm the transaction on Base. BaseScan shows the fee split between the payee and the XyncPay fee wallet in a single atomic transaction.
console.log(`View on BaseScan: https://basescan.org/tx/${receipt.hash}`);Security
Authentication
XyncPay uses wallet-based authentication. Every request must include your wallet address and a signature over the request body. No API keys, no OAuth. Your wallet is your identity.
Required Headers
| Header | Description |
|---|---|
| X-Wallet-Address | Your Ethereum wallet address (checksummed). Used to identify the agent and look up session state. |
| X-Wallet-Signature | EIP-191 signature of the JSON-serialized request body. The server recovers the signer address and verifies it matches X-Wallet-Address. |
Signing a Request
Sign the exact raw JSON bytes you will send as the request body. The server verifies the signature against request.text() with no normalization, so any difference in whitespace, key order, or field encoding between what you signed and what you sent will cause signature verification to fail.
import { ethers } from "ethers";
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
const body = JSON.stringify({
sourceProtocol: "x402",
targetProtocol: "mpp",
payeeAddress: "0xRecipientAddress",
amount: "1000000",
currency: "USDC",
chain: "base",
});
const signature = await wallet.signMessage(body);
const response = await fetch(`${process.env.XYNCPAY_API_URL}/api/v1/payments/translate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Wallet-Address": wallet.address,
"X-Wallet-Signature": signature,
},
body,
});Core Concepts
Translation Flow
Every payment follows a six-step lifecycle. Steps 2 and 6 are optional depending on the target protocol.
Register Agent
Registration is a two-step challenge-response flow. No auth headers are required on either step. Step 1 returns a short-lived challenge string. Step 2 submits your EIP-191 signature of that string. The server verifies wallet ownership and returns your xyncId.
// Step 1: request a challenge
const r1 = await fetch(`${API}/api/v1/agents/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
walletAddress: wallet.address,
preferredChain: "base",
}),
});
const { data: { challenge, nonce } } = await r1.json();
// Step 2: sign the challenge and complete registration
const signature = await wallet.signMessage(challenge);
const r2 = await fetch(`${API}/api/v1/agents/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
walletAddress: wallet.address,
preferredChain: "base",
signature,
nonce,
name: "trading-agent-alpha",
supportedProtocols: ["x402", "mpp"],
supportedChains: ["base"],
}),
});
const { data: agent } = await r2.json();
console.log("xyncId:", agent.xyncId); // e.g. "xync_6b8dd882f7a94bcb"Create Session (MPP only)
MPP requires a session context to track spending caps and multi-party state. Sessions are optional for x402 point-to-point translations.
const sessionBody = JSON.stringify({
agentId: agent.xyncId,
spendingCap: "50000000", // 50 USDC (6 decimals)
perTransactionLimit: "5000000", // 5 USDC max per tx
rateLimit: 30, // max 30 tx/min
allowedChains: ["base"],
allowedCurrencies: ["USDC"],
expiresInSeconds: 3600,
});
const sessionRes = await fetch(`${API}/api/v1/sessions/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Wallet-Address": wallet.address,
"X-Wallet-Signature": await wallet.signMessage(sessionBody),
},
body: sessionBody,
});
const { data: session } = await sessionRes.json();
console.log("sessionId:", session.sessionId);Translate Payment
Submit the payment details along with source and target protocols. The API constructs the appropriate on-chain call and returns an unsigned transaction.
const translateBody = JSON.stringify({
sourceProtocol: "x402",
targetProtocol: "mpp",
sessionId: session.sessionId,
payeeAddress: "0xMerchantAddress",
amount: "1000000",
currency: "USDC",
chain: "base",
});
const translateRes = await fetch(`${API}/api/v1/payments/translate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Wallet-Address": wallet.address,
"X-Wallet-Signature": await wallet.signMessage(translateBody),
},
body: translateBody,
});
const { data: payment } = await translateRes.json();
console.log("Fee:", payment.request.feeAmount);Sign the Unsigned Transaction
The unsigned transaction is a standard Ethereum transaction object. Sign it with your wallet. XyncPay never has access to your private key.
const signedTx = await wallet.signTransaction(payment.unsignedTransaction);Submit to Chain
Broadcast the signed transaction directly to the Base network. You can use any RPC provider; the transaction is self-contained.
const provider = new ethers.JsonRpcProvider("https://mainnet.base.org");
const txResponse = await provider.broadcastTransaction(signedTx);
const receipt = await txResponse.wait();
console.log("Tx hash:", receipt.hash);
console.log("Block:", receipt.blockNumber);Confirm Settlement
Optionally confirm settlement with the XyncPay API. This updates the session spending totals and triggers any webhook notifications.
const confirmBody = JSON.stringify({
txHash: receipt.hash,
});
await fetch(`${API}/api/v1/payments/${payment.paymentId}/confirm`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Wallet-Address": wallet.address,
"X-Wallet-Signature": await wallet.signMessage(confirmBody),
},
body: confirmBody,
});Protocols
Protocol Support
XyncPay translates between three payment protocols. Each protocol has different capabilities, settlement mechanisms, and chain requirements.
x402
Coinbase
HTTP-native micropayment protocol using EIP-3009 transferWithAuthorization. Designed for pay-per-request API access on Base.
- ● Full inbound support
- ● Full outbound support
- ● Base chain (8453)
- ● EIP-3009 authorization
MPP
Stripe / Tempo
Multi-party payment protocol with session-based spending caps. Supports complex payment flows with multiple participants and approval chains.
- ● Full inbound support
- ● Full outbound support
- ● Session-based context
- ● Spending caps & limits
AP2
Agent Payment Protocol from Google. Inbound translations are routed through x402 or MPP as the settlement layer. Outbound support is on the roadmap.
- ● Inbound support
- ○ Outbound (planned)
- ● Routes via x402 / MPP
- ● Google agent identity
Translation Matrix
| From → To | x402 | MPP | AP2 |
|---|---|---|---|
| x402 | - | ✓ | - |
| MPP | ✓ | - | - |
| AP2 | ✓ | ✓ | - |
Pricing
Fee Calculation
XyncPay charges a flat 1% translation fee with a $0.001 floor and a $1.00 cap. Fees are settled atomically on-chain via the FeeSplit contract. The recipient and XyncPay are paid in the same transaction.
Formula
fee = max(amount * 0.01, 0.001)
fee = min(fee, 1.00)
// Equivalent:
// fee = clamp(amount * 1% / 100 scaled, $0.001, $1.00)Examples
| Payment Amount | Calculated Fee | Applied Fee | Rule |
|---|---|---|---|
| $0.10 | $0.001 | $0.001 | Floor applied |
| $1.00 | $0.01 | $0.01 | Standard 1% |
| $10.00 | $0.10 | $0.10 | Standard 1% |
| $50.00 | $0.50 | $0.50 | Standard 1% |
| $200.00 | $2.00 | $1.00 | Cap applied |
On-Chain Fee Split
The FeeSplit contract handles atomic distribution in a single transaction. The translated payment and the protocol fee are settled together. If either transfer fails, the entire transaction reverts. This eliminates the risk of partial settlement.
// FeeSplit contract (simplified)
// Both transfers execute atomically in the translated transaction
struct Split {
address recipient; // merchant / service provider
uint256 amount; // payment amount minus fee
address feeCollector; // XyncPay fee address
uint256 feeAmount; // calculated fee
}
// The unsignedTransaction returned by /api/v1/payments/translate
// already encodes the FeeSplit call. No extra work needed.Reference
Error Handling
All error responses follow a consistent structure. The top-level error object contains a machine-readable code, a human-readable message, and optional details for validation errors.
Error Response Format
{
"error": {
"code": "INVALID_SIGNATURE",
"message": "Wallet signature verification failed"
}
}Error Codes
| Code | HTTP | Description |
|---|---|---|
| VALIDATION_ERROR | 422 | The request body failed schema validation. The response message identifies the invalid field. |
| INVALID_SIGNATURE | 401 | The X-Wallet-Signature header is missing or does not match the request body and wallet address. |
| CHALLENGE_EXPIRED | 401 | The registration challenge from step 1 has expired. Request a fresh challenge and complete registration again. |
| SESSION_EXHAUSTED | 403 | The session's cumulative spending cap has been reached. Create a new session or increase the cap. |
| SESSION_EXPIRED | 403 | The spending session has expired. Create a new session to continue. |
| SESSION_REVOKED | 403 | The spending session has been explicitly revoked. Create a new session to resume payments. |
| SCOPE_VIOLATION | 403 | The request violates a session restriction such as allowedChains or allowedCurrencies. The response message identifies the violated field. |
| PER_TRANSACTION_LIMIT | 403 | The transaction amount exceeds the session's per-transaction limit. Reduce the amount or create a session with a higher limit. |
| RATE_LIMIT_EXCEEDED | 429 | Too many requests. Back off and retry with exponential delay. The Retry-After header indicates wait time in seconds. |
| AGENT_NOT_FOUND | 404 | No agent is registered for the provided wallet address. Call /v1/agents/register first. |
| SESSION_NOT_FOUND | 404 | The sessionId does not match any known session. Verify the ID or create a new session. |
| PAYMENT_NOT_FOUND | 404 | The payment ID does not match any known translation request. |
| AGENT_EXISTS | 409 | An agent is already registered for this wallet address. Each wallet can register one agent. |
| CHAIN_MISMATCH | 400 | Cross-chain settlement is not supported. The sourceChain and targetChain must be the same. |
| UNSUPPORTED_PROTOCOL | 400 | The requested source or target protocol is not supported. |
| TRANSACTION_EXPIRED | 400 | The payment translation request has expired and can no longer be confirmed. Submit a new translation request. |
| NOT_IMPLEMENTED | 501 | The requested feature is not yet available. The response message identifies which feature. |
| INTERNAL_ERROR | 500 | An unexpected server error occurred. Retry the request after a brief delay. |
Handling Errors in Code
const response = await fetch(`${API}/v1/payments/translate`, {
method: "POST",
headers: { /* ... */ },
body: translateBody,
});
if (!response.ok) {
const { error } = await response.json();
switch (error.code) {
case "SESSION_EXPIRED":
case "SESSION_EXHAUSTED":
// create a new session and retry
break;
case "RATE_LIMIT_EXCEEDED":
const retryAfter = response.headers.get("Retry-After");
await new Promise((r) => setTimeout(r, Number(retryAfter) * 1000));
// retry the request
break;
default:
throw new Error(`XyncPay error [${error.code}]: ${error.message}`);
}
}