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

  1. Error Handling: Use Result<T, E> and proper error types

  2. Async/Await: Leverage Tokio for concurrent operations

  3. Type Safety: Use strong typing with Serde for serialization

  4. Environment Variables: Store API keys securely

  5. Logging: Use the tracing crate for structured logging

  6. Testing: Write unit and integration tests

  7. Performance: Use connection pooling and caching

Next Steps

Last updated