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.
1. Install the SDK
npm install @xyncpay/sdk ethers2. Configure Environment
Set your API endpoint and wallet address. XyncPay never holds your private key. All signing happens client-side.
# .env.local
XYNCPAY_API_URL=https://api.xyncpay.com
WALLET_ADDRESS=0xYourWalletAddress3. 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 response = await fetch(`${process.env.XYNCPAY_API_URL}/v1/payments/translate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Wallet-Address": process.env.WALLET_ADDRESS,
},
body: JSON.stringify({
source_protocol: "x402",
target_protocol: "mpp",
amount: "10.00",
currency: "USDC",
recipient: "0xRecipientAddress",
chain_id: 8453,
metadata: {
agent_id: "agent-001",
purpose: "api-access",
},
}),
});
const { unsigned_tx, fee, session_id } = await response.json();
console.log("Fee:", fee.amount, fee.currency);
console.log("Transaction ready for signing");4. Sign & Submit
Sign the returned transaction with your wallet and submit it to the chain. XyncPay never touches your private key.
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://mainnet.base.org");
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const signedTx = await wallet.signTransaction(unsigned_tx);
const txResponse = await provider.broadcastTransaction(signedTx);
const receipt = await txResponse.wait();
console.log("Settled:", 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 JSON string you send as the request body. The server will deserialize and re-serialize with deterministic key ordering, so use sorted keys when signing.
import { ethers } from "ethers";
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
const body = JSON.stringify({
amount: "10.00",
currency: "USDC",
recipient: "0xRecipientAddress",
source_protocol: "x402",
target_protocol: "mpp",
});
const signature = await wallet.signMessage(body);
const response = await fetch(`${process.env.XYNCPAY_API_URL}/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
Register your agent's wallet address. This creates an on-chain identity that the protocol uses to track sessions and enforce spending limits.
const res = await fetch(`${API}/v1/agents/register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Wallet-Address": wallet.address,
"X-Wallet-Signature": await wallet.signMessage(body),
},
body: JSON.stringify({
wallet_address: wallet.address,
label: "trading-agent-alpha",
allowed_protocols: ["x402", "mpp"],
spending_limit: "1000.00",
spending_limit_currency: "USDC",
}),
});
const { agent_id, registered_at } = await res.json();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({
agent_id,
protocol: "mpp",
spending_cap: "500.00",
currency: "USDC",
ttl_seconds: 3600,
});
const sessionRes = await fetch(`${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 { session_id, expires_at } = await sessionRes.json();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({
source_protocol: "x402",
target_protocol: "mpp",
session_id,
amount: "25.00",
currency: "USDC",
recipient: "0xMerchantAddress",
chain_id: 8453,
});
const translateRes = await fetch(`${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 { unsigned_tx, fee, payment_id } = await translateRes.json();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(unsigned_tx);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({
payment_id,
tx_hash: receipt.hash,
block_number: receipt.blockNumber,
});
await fetch(`${API}/v1/payments/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 unsigned_tx returned by /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": "INSUFFICIENT_BALANCE",
"message": "Wallet 0xAbc...def has 4.20 USDC but 25.00 USDC is required.",
"details": {
"wallet": "0xAbcdef1234567890abcdef1234567890abcdef12",
"available": "4.20",
"required": "25.00",
"currency": "USDC"
}
}
}Error Codes
| Code | HTTP | Description |
|---|---|---|
| INVALID_SIGNATURE | 401 | The X-Wallet-Signature header is missing or does not match the request body and wallet address. |
| AGENT_NOT_FOUND | 404 | No agent is registered for the provided wallet address. Call /v1/agents/register first. |
| SESSION_EXPIRED | 410 | The MPP session has exceeded its TTL. Create a new session to continue. |
| SPENDING_CAP_EXCEEDED | 403 | The requested amount would exceed the session or agent spending cap. |
| INSUFFICIENT_BALANCE | 402 | The wallet does not hold enough USDC to cover the payment amount plus fees. |
| UNSUPPORTED_TRANSLATION | 400 | The requested source → target protocol combination is not supported. |
| CHAIN_NOT_SUPPORTED | 400 | The provided chain_id is not supported. XyncPay currently operates on Base (chain_id: 8453). Arc support is coming soon. |
| RATE_LIMITED | 429 | Too many requests. Back off and retry with exponential delay. The Retry-After header indicates wait time in seconds. |
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 "INSUFFICIENT_BALANCE":
console.error(`Need ${error.details.required} ${error.details.currency}, have ${error.details.available}`);
break;
case "SESSION_EXPIRED":
// create a new session and retry
break;
case "RATE_LIMITED":
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}`);
}
}