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 ethers

2. 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=0xYourPrivateKey

3. 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_6b8dd882f7a94bcb

4. 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_488b8111cdaa4539a5be

5. 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

HeaderDescription
X-Wallet-AddressYour Ethereum wallet address (checksummed). Used to identify the agent and look up session state.
X-Wallet-SignatureEIP-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.

1

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"
2

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);
3

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);
4

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);
5

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);
6

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

Google

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 → Tox402MPPAP2
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 AmountCalculated FeeApplied FeeRule
$0.10$0.001$0.001Floor applied
$1.00$0.01$0.01Standard 1%
$10.00$0.10$0.10Standard 1%
$50.00$0.50$0.50Standard 1%
$200.00$2.00$1.00Cap 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

CodeHTTPDescription
VALIDATION_ERROR422The request body failed schema validation. The response message identifies the invalid field.
INVALID_SIGNATURE401The X-Wallet-Signature header is missing or does not match the request body and wallet address.
CHALLENGE_EXPIRED401The registration challenge from step 1 has expired. Request a fresh challenge and complete registration again.
SESSION_EXHAUSTED403The session's cumulative spending cap has been reached. Create a new session or increase the cap.
SESSION_EXPIRED403The spending session has expired. Create a new session to continue.
SESSION_REVOKED403The spending session has been explicitly revoked. Create a new session to resume payments.
SCOPE_VIOLATION403The request violates a session restriction such as allowedChains or allowedCurrencies. The response message identifies the violated field.
PER_TRANSACTION_LIMIT403The transaction amount exceeds the session's per-transaction limit. Reduce the amount or create a session with a higher limit.
RATE_LIMIT_EXCEEDED429Too many requests. Back off and retry with exponential delay. The Retry-After header indicates wait time in seconds.
AGENT_NOT_FOUND404No agent is registered for the provided wallet address. Call /v1/agents/register first.
SESSION_NOT_FOUND404The sessionId does not match any known session. Verify the ID or create a new session.
PAYMENT_NOT_FOUND404The payment ID does not match any known translation request.
AGENT_EXISTS409An agent is already registered for this wallet address. Each wallet can register one agent.
CHAIN_MISMATCH400Cross-chain settlement is not supported. The sourceChain and targetChain must be the same.
UNSUPPORTED_PROTOCOL400The requested source or target protocol is not supported.
TRANSACTION_EXPIRED400The payment translation request has expired and can no longer be confirmed. Submit a new translation request.
NOT_IMPLEMENTED501The requested feature is not yet available. The response message identifies which feature.
INTERNAL_ERROR500An 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}`);
  }
}