Rust
Complete examples for accepting x402 payments with Interface402 using Rust.
Dependencies
Add to Cargo.toml:
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
tokio-tungstenite = "0.21" # For WebSocket support
futures-util = "0.3"Basic Payment Verification
Verify a Payment
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::env;
use anyhow::Result;
#[derive(Serialize)]
struct PaymentVerification {
payment_proof: String,
amount: u64,
recipient: String,
}
#[derive(Deserialize, Debug)]
struct VerificationResponse {
verified: bool,
transaction_id: Option<String>,
amount: Option<u64>,
#[serde(default)]
error: Option<String>,
}
async fn verify_payment(
payment_proof: &str,
amount: u64,
recipient: &str,
) -> Result<VerificationResponse> {
let client = Client::new();
let api_key = env::var("API_KEY")?;
let request = PaymentVerification {
payment_proof: payment_proof.to_string(),
amount,
recipient: recipient.to_string(),
};
let response = client
.post("https://api.interface402.dev/v1/payments/verify")
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.json(&request)
.send()
.await?;
let data: VerificationResponse = response.json().await?;
if data.verified {
println!("✅ Payment verified!");
println!("Transaction ID: {:?}", data.transaction_id);
println!("Amount: {:?}", data.amount);
} else {
println!("❌ Payment invalid");
}
Ok(data)
}
#[tokio::main]
async fn main() -> Result<()> {
let result = verify_payment(
"BASE64_ENCODED_PAYMENT_PROOF",
1000000,
"YOUR_WALLET_ADDRESS",
)
.await?;
println!("Result: {:?}", result);
Ok(())
}Axum Web Framework Integration
Protected API Endpoint
use axum::{
extract::{Request, State},
http::{HeaderMap, StatusCode},
middleware::{self, Next},
response::{IntoResponse, Json, Response},
routing::get,
Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
api_key: String,
wallet_address: String,
}
#[derive(Serialize)]
struct PaymentRequired {
error: String,
amount: u64,
recipient: String,
}
#[derive(Deserialize)]
struct VerificationResponse {
verified: bool,
}
async fn verify_payment_with_interface402(
payment_proof: &str,
amount: u64,
recipient: &str,
api_key: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let response = client
.post("https://api.interface402.dev/v1/payments/verify")
.header("Authorization", format!("Bearer {}", api_key))
.json(&serde_json::json!({
"payment_proof": payment_proof,
"amount": amount,
"recipient": recipient
}))
.send()
.await?;
let data: VerificationResponse = response.json().await?;
Ok(data.verified)
}
// Middleware to require payment
async fn payment_middleware(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
request: Request,
next: Next,
) -> Response {
const REQUIRED_AMOUNT: u64 = 1000000;
let payment_proof = headers
.get("x-payment-proof")
.and_then(|h| h.to_str().ok());
if payment_proof.is_none() {
return (
StatusCode::PAYMENT_REQUIRED,
Json(PaymentRequired {
error: "Payment Required".to_string(),
amount: REQUIRED_AMOUNT,
recipient: state.wallet_address.clone(),
}),
)
.into_response();
}
// Verify payment
match verify_payment_with_interface402(
payment_proof.unwrap(),
REQUIRED_AMOUNT,
&state.wallet_address,
&state.api_key,
)
.await
{
Ok(true) => next.run(request).await,
_ => (
StatusCode::PAYMENT_REQUIRED,
Json(serde_json::json!({
"error": "Invalid payment"
})),
)
.into_response(),
}
}
// Protected route
async fn premium_content() -> Json<serde_json::Value> {
Json(serde_json::json!({
"data": "Your premium content here",
"message": "Access granted"
}))
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState {
api_key: std::env::var("API_KEY").unwrap(),
wallet_address: "YOUR_WALLET_ADDRESS".to_string(),
});
let app = Router::new()
.route("/api/premium-content", get(premium_content))
.layer(middleware::from_fn_with_state(state.clone(), payment_middleware))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}Actix-Web Integration
Payment-Protected Endpoint
use actix_web::{
get, middleware, web, App, HttpRequest, HttpResponse, HttpServer, Result,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct PaymentRequired {
error: String,
amount: u64,
recipient: String,
}
#[derive(Deserialize)]
struct VerificationResponse {
verified: bool,
}
async fn verify_payment(
payment_proof: &str,
amount: u64,
recipient: &str,
api_key: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let response = client
.post("https://api.interface402.dev/v1/payments/verify")
.header("Authorization", format!("Bearer {}", api_key))
.json(&serde_json::json!({
"payment_proof": payment_proof,
"amount": amount,
"recipient": recipient
}))
.send()
.await?;
let data: VerificationResponse = response.json().await?;
Ok(data.verified)
}
#[get("/api/premium-content")]
async fn premium_content(req: HttpRequest) -> Result<HttpResponse> {
const REQUIRED_AMOUNT: u64 = 1000000;
const WALLET_ADDRESS: &str = "YOUR_WALLET_ADDRESS";
let payment_proof = req
.headers()
.get("x-payment-proof")
.and_then(|h| h.to_str().ok());
if payment_proof.is_none() {
return Ok(HttpResponse::PaymentRequired().json(PaymentRequired {
error: "Payment Required".to_string(),
amount: REQUIRED_AMOUNT,
recipient: WALLET_ADDRESS.to_string(),
}));
}
let api_key = std::env::var("API_KEY").unwrap();
match verify_payment(
payment_proof.unwrap(),
REQUIRED_AMOUNT,
WALLET_ADDRESS,
&api_key,
)
.await
{
Ok(true) => Ok(HttpResponse::Ok().json(serde_json::json!({
"data": "Your premium content here",
"message": "Access granted"
}))),
_ => Ok(HttpResponse::PaymentRequired().json(serde_json::json!({
"error": "Invalid payment"
}))),
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Server running on http://localhost:3000");
HttpServer::new(|| {
App::new()
.wrap(middleware::Logger::default())
.service(premium_content)
})
.bind(("127.0.0.1", 3000))?
.run()
.await
}WebSocket for Real-Time Notifications
Basic WebSocket Connection
use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use tokio_tungstenite::{connect_async, tungstenite::Message};
#[derive(Serialize)]
struct AuthMessage {
#[serde(rename = "type")]
msg_type: String,
#[serde(rename = "apiKey")]
api_key: String,
}
#[derive(Serialize)]
struct SubscribeMessage {
#[serde(rename = "type")]
msg_type: String,
wallet: String,
}
#[derive(Deserialize, Debug)]
struct PaymentNotification {
transaction_id: String,
amount: u64,
sender: String,
}
async fn stream_payments(api_key: &str, wallet_address: &str) -> anyhow::Result<()> {
let url = "wss://api.interface402.dev/v1/payments/stream";
println!("Connecting to payment stream...");
let (ws_stream, _) = connect_async(url).await?;
println!("Connected!");
let (mut write, mut read) = ws_stream.split();
// Authenticate
let auth_msg = AuthMessage {
msg_type: "authenticate".to_string(),
api_key: api_key.to_string(),
};
write
.send(Message::Text(serde_json::to_string(&auth_msg)?))
.await?;
// Subscribe to payments
let sub_msg = SubscribeMessage {
msg_type: "subscribe".to_string(),
wallet: wallet_address.to_string(),
};
write
.send(Message::Text(serde_json::to_string(&sub_msg)?))
.await?;
println!("Listening for payments...");
// Listen for messages
while let Some(msg) = read.next().await {
match msg {
Ok(Message::Text(text)) => {
if let Ok(payment) = serde_json::from_str::<PaymentNotification>(&text) {
println!("💰 Payment received!");
println!(" Amount: {}", payment.amount);
println!(" From: {}", payment.sender);
println!(" Transaction: {}", payment.transaction_id);
}
}
Ok(Message::Close(_)) => {
println!("Connection closed");
break;
}
Err(e) => {
eprintln!("Error: {}", e);
break;
}
_ => {}
}
}
Ok(())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let api_key = std::env::var("API_KEY")?;
let wallet = "YOUR_WALLET_ADDRESS";
stream_payments(&api_key, wallet).await?;
Ok(())
}Error Handling
Robust Error Handling with Retry
use anyhow::{anyhow, Result};
use std::time::Duration;
use tokio::time::sleep;
#[derive(Debug)]
enum PaymentError {
InvalidProof,
InvalidAmount,
NetworkError,
RateLimited,
Unknown(String),
}
impl std::fmt::Display for PaymentError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
PaymentError::InvalidProof => write!(f, "Invalid payment proof"),
PaymentError::InvalidAmount => write!(f, "Invalid payment amount"),
PaymentError::NetworkError => write!(f, "Network error"),
PaymentError::RateLimited => write!(f, "Rate limited"),
PaymentError::Unknown(msg) => write!(f, "Unknown error: {}", msg),
}
}
}
impl std::error::Error for PaymentError {}
async fn verify_payment_with_retry(
payment_proof: &str,
amount: u64,
recipient: &str,
max_retries: u32,
) -> Result<bool> {
let client = reqwest::Client::new();
let api_key = std::env::var("API_KEY")?;
for attempt in 0..max_retries {
let response = client
.post("https://api.interface402.dev/v1/payments/verify")
.header("Authorization", format!("Bearer {}", api_key))
.json(&serde_json::json!({
"payment_proof": payment_proof,
"amount": amount,
"recipient": recipient
}))
.send()
.await;
match response {
Ok(resp) => {
if resp.status().is_success() {
let data: serde_json::Value = resp.json().await?;
return Ok(data["verified"].as_bool().unwrap_or(false));
} else if resp.status() == 429 {
// Rate limited, retry
if attempt < max_retries - 1 {
let delay = Duration::from_secs(2u64.pow(attempt));
println!("Rate limited, retrying in {:?}...", delay);
sleep(delay).await;
continue;
}
return Err(anyhow!(PaymentError::RateLimited));
} else {
let error: serde_json::Value = resp.json().await?;
let code = error["error"]["code"].as_str().unwrap_or("UNKNOWN");
return match code {
"INVALID_PAYMENT_PROOF" => Err(anyhow!(PaymentError::InvalidProof)),
"INVALID_AMOUNT" => Err(anyhow!(PaymentError::InvalidAmount)),
_ => Err(anyhow!(PaymentError::Unknown(code.to_string()))),
};
}
}
Err(e) => {
if attempt < max_retries - 1 {
let delay = Duration::from_secs(2u64.pow(attempt));
println!("Network error, retrying in {:?}...", delay);
sleep(delay).await;
continue;
}
return Err(anyhow!(PaymentError::NetworkError));
}
}
}
Err(anyhow!("Max retries exceeded"))
}
#[tokio::main]
async fn main() -> Result<()> {
match verify_payment_with_retry(
"BASE64_ENCODED_PAYMENT_PROOF",
1000000,
"YOUR_WALLET_ADDRESS",
3,
)
.await
{
Ok(verified) => {
if verified {
println!("Payment verified!");
} else {
println!("Payment invalid");
}
}
Err(e) => {
eprintln!("Verification failed: {}", e);
}
}
Ok(())
}Complete Example: Payment-Protected API
use axum::{
extract::State,
http::{HeaderMap, StatusCode},
response::{IntoResponse, Json},
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, sync::Arc};
use tokio::sync::RwLock;
#[derive(Clone)]
struct AppState {
api_key: String,
wallet_address: String,
verified_payments: Arc<RwLock<HashSet<String>>>,
}
#[derive(Deserialize)]
struct VerifyPaymentRequest {
payment_proof: String,
amount: u64,
}
#[derive(Serialize)]
struct VerifyPaymentResponse {
success: bool,
transaction_id: Option<String>,
}
async fn verify_payment_endpoint(
State(state): State<Arc<AppState>>,
Json(payload): Json<VerifyPaymentRequest>,
) -> impl IntoResponse {
let client = reqwest::Client::new();
let response = client
.post("https://api.interface402.dev/v1/payments/verify")
.header("Authorization", format!("Bearer {}", state.api_key))
.json(&serde_json::json!({
"payment_proof": payload.payment_proof,
"amount": payload.amount,
"recipient": state.wallet_address
}))
.send()
.await;
match response {
Ok(resp) => {
let data: serde_json::Value = resp.json().await.unwrap();
if data["verified"].as_bool().unwrap_or(false) {
let tx_id = data["transaction_id"].as_str().unwrap().to_string();
// Cache the transaction ID
state.verified_payments.write().await.insert(tx_id.clone());
(
StatusCode::OK,
Json(VerifyPaymentResponse {
success: true,
transaction_id: Some(tx_id),
}),
)
} else {
(
StatusCode::PAYMENT_REQUIRED,
Json(VerifyPaymentResponse {
success: false,
transaction_id: None,
}),
)
}
}
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(VerifyPaymentResponse {
success: false,
transaction_id: None,
}),
),
}
}
async fn get_content(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> impl IntoResponse {
let transaction_id = headers
.get("x-transaction-id")
.and_then(|h| h.to_str().ok());
if let Some(tx_id) = transaction_id {
if state.verified_payments.read().await.contains(tx_id) {
return (
StatusCode::OK,
Json(serde_json::json!({
"content": "Your premium content here"
})),
);
}
}
(
StatusCode::PAYMENT_REQUIRED,
Json(serde_json::json!({
"error": "Payment Required",
"message": "Valid payment required to access this content"
})),
)
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState {
api_key: std::env::var("API_KEY").unwrap(),
wallet_address: "YOUR_WALLET_ADDRESS".to_string(),
verified_payments: Arc::new(RwLock::new(HashSet::new())),
});
let app = Router::new()
.route("/api/verify-payment", post(verify_payment_endpoint))
.route("/api/content", get(get_content))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}Best Practices
Error Handling: Use
Result<T, E>and proper error typesAsync/Await: Leverage Tokio for concurrent operations
Type Safety: Use strong typing with Serde for serialization
Environment Variables: Store API keys securely
Logging: Use the
tracingcrate for structured loggingTesting: Write unit and integration tests
Performance: Use connection pooling and caching
Next Steps
See JavaScript examples for JavaScript/TypeScript integration
See Python examples for Python integration
Check out the API Reference for all endpoints
Read about error handling best practices
Last updated
