Skip to main content

Overview

Beacon Cloud uses on-chain cryptocurrency payment verification to provide paid AI inference services. Payments are accepted in USDC on Base and Solana networks, with real-time transaction verification before processing requests.
Payment verification is only required when using the beacon-ai-cloud provider. Self-hosted instances can disable this entirely.

Architecture

Payment Flow

1. Initial Request (No Payment)

When a client requests generation with provider: "beacon-ai-cloud" but no payment headers:
curl -X POST https://beacon-api.com/generate \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-repo",
    "provider": "beacon-ai-cloud",
    "source_files": [...]
  }'
Response (402 Payment Required):
HTTP/1.1 402 Payment Required
x-payment-run-id: 550e8400-e29b-41d4-a716-446655440000
x-payment-amount: 0.09
x-payment-currency: USDC
x-payment-address-base: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
x-payment-address-solana: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v

{
  "success": false,
  "error": "Payment required"
}
Implementation in src/main.rs:248-261:
let rid = db::create_run(&req.repo_context.name).await?;
let amount = std::env::var("PAYMENT_AMOUNT_USDC")
    .unwrap_or_else(|_| "0.09".to_string());
let w_base = std::env::var("BEACON_WALLET_BASE").unwrap_or_default();
let w_sol = std::env::var("BEACON_WALLET_SOLANA").unwrap_or_default();

return Err(errors::BeaconError::PaymentRequired {
    run_id: rid,
    amount,
    base_addr: w_base,
    sol_addr: w_sol,
});

2. Client Sends Payment

The client sends USDC to one of the provided addresses:
# Using a wallet SDK or CLI
cast send 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
  "transfer(address,uint256)" \
  <BEACON_WALLET> \
  90000  # 0.09 USDC (6 decimals)

3. Retry with Transaction Hash

After the transaction confirms, the client retries with payment headers:
curl -X POST https://beacon-api.com/generate \
  -H "Content-Type: application/json" \
  -H "x-payment-run-id: 550e8400-e29b-41d4-a716-446655440000" \
  -H "x-payment-chain: base" \
  -H "x-payment-txn-hash: 0xabcd1234..." \
  -d '{...}'

Transaction Verification

Base (Ethereum L2)

Beacon verifies Base transactions by:
  1. Fetching the transaction receipt via RPC
  2. Checking transaction status is successful
  3. Finding USDC Transfer events
  4. Validating recipient and amount
Implementation (src/verifier.rs:23-52):
async fn verify_base(
    txn_hash: &str,
    expected_amount: f64,
    expected_address: &str,
) -> Result<bool> {
    let rpc_url = std::env::var("BASE_RPC_URL")
        .unwrap_or_else(|_| "https://mainnet.base.org".to_string());
    let provider = Provider::<Http>::try_from(rpc_url)?;
    
    // Get transaction receipt
    let hash = H256::from_str(txn_hash)?;
    let receipt = provider.get_transaction_receipt(hash).await?
        .context("Base transaction receipt not found")?;
    
    // Check transaction succeeded
    if receipt.status != Some(1.into()) {
        return Ok(false);
    }
    
    // Verify USDC transfer event
    let usdc_addr = Address::from_str(BASE_USDC)?;  // 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
    let receiver_addr = Address::from_str(expected_address)?;
    let transfer_topic = H256::from_str(
        "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
    )?;
    
    for log in receipt.logs {
        if log.address == usdc_addr 
            && log.topics.len() == 3 
            && log.topics[0] == transfer_topic 
        {
            let to = Address::from_slice(&log.topics[2][12..]);
            if to == receiver_addr {
                let value = U256::from_big_endian(&log.data);
                let amount_f64 = value.as_u128() as f64 / 1_000_000.0;  // USDC has 6 decimals
                
                // Allow 0.001 USDC tolerance
                if (amount_f64 - expected_amount).abs() < 0.001 {
                    return Ok(true);
                }
            }
        }
    }
    Ok(false)
}
Key Details:
  • USDC Contract: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
  • Transfer Event: Transfer(address indexed from, address indexed to, uint256 value)
  • Decimals: 6 (divide by 1,000,000)
  • Tolerance: ±0.001 USDC

Solana

Solana verification uses JSON-RPC to:
  1. Fetch transaction details
  2. Compare pre/post token balances
  3. Validate the balance increase matches payment
Implementation (src/verifier.rs:54-94):
async fn verify_solana(
    txn_hash: &str,
    expected_amount: f64,
    expected_address: &str,
) -> Result<bool> {
    let rpc_url = std::env::var("SOLANA_RPC_URL")
        .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
    
    let client = reqwest::Client::new();
    let resp = client.post(rpc_url)
        .json(&json!({
            "jsonrpc": "2.0",
            "id": 1,
            "method": "getTransaction",
            "params": [
                txn_hash,
                { "encoding": "json", "maxSupportedTransactionVersion": 0 }
            ]
        }))
        .send().await?.json::<Value>().await?;
    
    let meta = &resp["result"]["meta"];
    if meta.is_null() { return Ok(false); }
    
    // Check pre/post token balances
    let pre = meta["preTokenBalances"].as_array()?;
    let post = meta["postTokenBalances"].as_array()?;
    
    let mut pre_val = 0.0;
    for b in pre {
        if b["mint"] == SOLANA_USDC && b["owner"] == expected_address {
            pre_val = b["uiTokenAmount"]["uiAmount"].as_f64().unwrap_or(0.0);
            break;
        }
    }
    
    let mut post_val = 0.0;
    for b in post {
        if b["mint"] == SOLANA_USDC && b["owner"] == expected_address {
            post_val = b["uiTokenAmount"]["uiAmount"].as_f64().unwrap_or(0.0);
            break;
        }
    }
    
    // Verify amount difference
    if (post_val - pre_val - expected_amount).abs() < 0.001 {
        return Ok(true);
    }
    Ok(false)
}
Key Details:
  • USDC Mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
  • Balance Comparison: postBalance - preBalance
  • Tolerance: ±0.001 USDC

Replay Attack Prevention

Beacon prevents transaction replay attacks by tracking used transaction hashes in the database. Check Implementation (src/main.rs:219-221 and src/db.rs:147-160):
if db::payment_already_used(txn).await.unwrap_or(false) {
    return Err(errors::BeaconError::TransactionAlreadyUsed);
}
pub async fn payment_already_used(txn_hash: &str) -> Result<bool> {
    let db = client()?;
    let resp = db.from(PAYMENTS_TABLE)
        .eq("txn_hash", txn_hash)
        .select("id")
        .execute()
        .await?;
    
    let body = resp.text().await?;
    let records: Value = serde_json::from_str(&body)?;
    Ok(records.as_array().map(|a| !a.is_empty()).unwrap_or(false))
}
Once verified, the transaction hash is recorded:
db::record_payment(rid, txn, ch, None).await.ok();

Database Schema

Runs Table

CREATE TABLE runs (
  id UUID PRIMARY KEY,
  repo_name TEXT NOT NULL,
  provider TEXT NOT NULL,
  status TEXT NOT NULL,  -- 'pending', 'paid', 'complete', 'failed'
  txn_hash TEXT,
  chain TEXT,
  agents_md TEXT,
  error TEXT,
  created_at TIMESTAMP DEFAULT NOW()
);

Payments Table

CREATE TABLE payments (
  id UUID PRIMARY KEY,
  run_id UUID REFERENCES runs(id),
  txn_hash TEXT UNIQUE NOT NULL,
  chain TEXT NOT NULL,
  amount_usdc NUMERIC NOT NULL,
  from_address TEXT,
  confirmed BOOLEAN DEFAULT true,
  confirmed_at TIMESTAMP DEFAULT NOW()
);

Error Handling

Transaction Not Found

HTTP/1.1 500 Internal Server Error
{
  "success": false,
  "error": "Verification failed: Base transaction receipt not found"
}

Insufficient Payment

HTTP/1.1 402 Payment Required
{
  "success": false,
  "error": "Payment not verified"
}

Transaction Already Used

HTTP/1.1 409 Conflict
{
  "success": false,
  "error": "Transaction hash already used"
}

Testing Payment Verification

Using Testnets

For development, use testnet configurations:
.env.test
BASE_RPC_URL=https://sepolia.base.org
SOLANA_RPC_URL=https://api.devnet.solana.com
BEACON_WALLET_BASE=0xYourTestWallet
BEACON_WALLET_SOLANA=YourDevnetWallet
PAYMENT_AMOUNT_USDC=0.01

Mock Verification

For local testing, you can bypass verification:
// In src/verifier.rs, add mock mode
pub async fn verify_payment(
    chain: &str,
    txn_hash: &str,
    expected_amount: f64,
    expected_address: &str,
) -> Result<bool> {
    if std::env::var("MOCK_PAYMENT_VERIFICATION").is_ok() {
        return Ok(true);  // Always verify in test mode
    }
    // ... existing verification logic
}

Security Considerations

Important security notes:
  • Always verify transaction finality (confirmations) before accepting
  • Use secure RPC endpoints (not public, rate-limited ones)
  • Implement idempotency to handle duplicate requests
  • Monitor for suspicious patterns (same address, rapid requests)
  • Keep wallet private keys in secure vaults (HSM, KMS)

Best Practices

  1. Confirmation Depth: Wait for sufficient confirmations
    • Base: 10+ blocks (~20 seconds)
    • Solana: Finalized commitment level
  2. RPC Redundancy: Use multiple RPC providers
    let providers = vec![
        "https://mainnet.base.org",
        "https://base-mainnet.g.alchemy.com/v2/YOUR_KEY",
    ];
    
  3. Amount Tolerance: Keep tight tolerance (0.001 USDC)
  4. Transaction Indexing: Index all payment transactions for auditing

Self-Hosted: Disabling Payments

To run Beacon without payment verification:
  1. Remove beacon-ai-cloud provider checks
  2. Use direct AI provider APIs (Gemini, Claude, OpenAI)
  3. Set your own API keys in environment variables
# Self-hosted configuration
GEMINI_API_KEY=your_key
REDIS_URL=redis://localhost:6379
# No SUPABASE or payment variables needed
Generate without payment:
beacon generate ./repo --provider gemini

Next Steps

Configuration

Configure payment wallets and RPC endpoints

Custom Deployment

Deploy with payment verification enabled

Build docs developers (and LLMs) love