Integration guide
This page covers integrating the confidential payment scheme into your own client application and API server. If you just want to run a quick end-to-end test using TACEO's hosted infrastructure, start with the Quickstart instead.
Install
- TypeScript
- Rust
npm install @taceo/confidential-x402 @x402/fetch viem
[dependencies]
alloy = "2"
eyre = "0.6"
axum = "0.8"
reqwest = { version = "0.13", features = ["json", "rustls"] }
taceo-merces1-x402 = { git = "https://github.com/TaceoLabs/merces1-x402.git", version = "0.1", features = ["server", "client"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
x402-axum = "1"
x402-reqwest = "1"
Client
The client library intercepts any 402 Payment Required response, constructs the ZK proof and
signed payment payload, and retries the request automatically. Your application code makes a
normal HTTP request, the payment is invisible to it.
Register the scheme
- TypeScript
- Rust
import { privateKeyToAccount } from 'viem/accounts';
import { x402Client, wrapFetchWithPayment } from '@x402/fetch';
import { ConfidentialEvmScheme } from '@taceo/confidential-x402/client';
const signer = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const client = new x402Client();
client.register('eip155:*', new ConfidentialEvmScheme(signer));
export const fetchWithPayment = wrapFetchWithPayment(fetch, client);
fetchWithPayment is a drop-in replacement for fetch. Use it anywhere you would normally
call fetch.
use alloy::signers::local::PrivateKeySigner;
use reqwest::Client;
use taceo_merces1_x402::V2Eip155ConfidentialClient;
use x402_reqwest::{ReqwestWithPayments, ReqwestWithPaymentsBuild, X402Client};
let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY")?.parse()?;
let x402_client = X402Client::new()
.register(V2Eip155ConfidentialClient::new(signer));
let http_client = Client::new()
.with_payments(x402_client)
.build();
http_client is a drop-in replacement for reqwest::Client.
Make a payment-gated request
- TypeScript
- Rust
const response = await fetchWithPayment(
'https://api.example.com/api/premium-data',
{ method: 'GET' }
);
if (response.ok) {
const data = await response.json();
console.log(data);
}
let response = http_client
.get("https://api.example.com/api/premium-data")
.send()
.await?;
println!("Status: {}", response.status());
println!("Body: {}", response.text().await?);
When the server responds with 402, the client handles it invisibly: parses the PaymentRequired
payload, generates the Groth16 ZK proof, retries the request with a PAYMENT-SIGNATURE
header, and returns the final 200 OK to your code.
Server
The server middleware intercepts incoming requests, returns 402 Payment Required when no valid
payment is attached, and verifies the payment against the TACEO facilitator before forwarding the
request to your handler.
Basic example
The following examples protect a single route at GET /api/protected and price it at $1 of
confidential USDC.
- TypeScript
- Rust
import express from 'express';
import { paymentMiddleware, x402ResourceServer } from '@x402/express';
import { HTTPFacilitatorClient } from '@x402/core/server';
import { ConfidentialEvmScheme } from '@taceo/confidential-x402/server';
const app = express();
const facilitatorClient = new HTTPFacilitatorClient({
url: 'https://facilitator.merces.taceo.io',
});
app.use(
paymentMiddleware(
{
'GET /api/protected': {
accepts: [
{
scheme: 'confidential',
price: '$1',
network: 'eip155:84532',
payTo: process.env.WALLET_ADDRESS as `0x${string}`,
},
],
},
},
new x402ResourceServer(facilitatorClient).register(
'eip155:84532',
new ConfidentialEvmScheme({ asset: process.env.TOKEN_ADDRESS as string })
)
)
);
app.get('/api/protected', (req, res) => {
res.json({ message: 'Payment verified. Here is your data.' });
});
app.listen(8080, () => console.log('Listening on http://localhost:8080'));
use std::str::FromStr;
use alloy::primitives::Address;
use axum::{Router, routing::get, response::IntoResponse, http::StatusCode};
use taceo_merces1_x402::{ConfidentialUSDC, V2Eip155Confidential};
use x402_axum::X402Middleware;
#[tokio::main]
async fn main() -> eyre::Result<()> {
let pay_to = Address::from_str(&std::env::var("WALLET_ADDRESS")?)?;
let facilitator_url = std::env::var("FACILITATOR_URL")
.unwrap_or("https://facilitator.merces.taceo.io".to_string());
let usdc = ConfidentialUSDC::base_sepolia();
let x402 = X402Middleware::new(&facilitator_url);
let app = Router::new().route(
"/api/protected",
get(handler).layer(
x402.with_price_tag(
V2Eip155Confidential::price_tag(pay_to, usdc.parse("$1")?)
),
),
);
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
println!("Listening on http://0.0.0.0:8080");
axum::serve(listener, app).await?;
Ok(())
}
async fn handler() -> impl IntoResponse {
(StatusCode::OK, "Payment verified. Here is your data.")
}
How the middleware works
When a request arrives without a valid PAYMENT-SIGNATURE header:
- The middleware returns
HTTP 402 Payment Requiredwith a base64-encodedPaymentRequiredpayload containing the scheme, required amount, your wallet address, and the MPC public keys (fetched from the facilitator's/supportedendpoint). - When the client retries with a payment, the middleware forwards it to the facilitator's
/verifyendpoint. - If verification succeeds, the request is forwarded to your handler.
- After your handler responds, the middleware calls
/settleasynchronously.
Protecting multiple routes
- TypeScript
- Rust
app.use(
paymentMiddleware(
{
'GET /api/basic': { accepts: [{ scheme: 'confidential', price: '$0.10', network: 'eip155:84532', payTo: address }] },
'GET /api/premium': { accepts: [{ scheme: 'confidential', price: '$5.00', network: 'eip155:84532', payTo: address }] },
'POST /api/action': { accepts: [{ scheme: 'confidential', price: '$1.00', network: 'eip155:84532', payTo: address }] },
},
resourceServer
)
);
let app = Router::new()
.route("/api/basic",
get(basic_handler).layer(
x402.with_price_tag(V2Eip155Confidential::price_tag(pay_to, usdc.parse("$0.10")?))
)
)
.route("/api/premium",
get(premium_handler).layer(
x402.with_price_tag(V2Eip155Confidential::price_tag(pay_to, usdc.parse("$5.00")?))
)
);
Configuration reference
| Parameter | Description |
|---|---|
scheme | Must be "confidential" |
price | Required payment amount, e.g. "$1", "$0.50" |
network | CAIP-2 chain ID, e.g. "eip155:84532" for Base Sepolia |
payTo | Your wallet address that receives payments |
asset | Token contract address (provided by the TACEO facilitator automatically) |
| Facilitator URL | https://facilitator.merces.taceo.io for TACEO's hosted facilitator |
Payments are credited to your payTo address as confidential Merces balance. To withdraw tokens
back to a regular ERC-20 balance, use the withdraw function in the Merces client library.