walletpress/backend/x402_service.py
cryptorugmunch e13bd4d774
Some checks are pending
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / security (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / license (push) Waiting to run
CI / ai-review (push) Waiting to run
docs: apply fleet-template (16-artifact scaffold)
Adds missing standard artifacts:
- README.md (if missing)
- AGENTS.md (AI agent contract)
- PLAN.md (current sprint)
- STATUS.md (where we are)
- DEVELOPMENT.md (dev workflow)
- DEPLOYMENT.md (deploy procedure)
- TESTING.md (test strategy)
- DECISIONS.md (ADR index + templates)
- .github/CODEOWNERS
- .github/workflows/ci.yml

Preserves all existing artifacts.

Refs: RugMunchMedia/fleet-template
2026-07-02 02:07:06 +07:00

372 lines
15 KiB
Python

#!/usr/bin/env python3
"""WalletPress x402 Marketplace — standalone pay-per-wallet service.
THIS IS NOT PART OF THE SELF-HOSTED PRODUCT.
This is a service WE operate at walletpress.cc.
The x402 marketplace allows bots and developers to generate wallets
on demand without an account or subscription. They pay USDC per wallet,
we generate and return the keys, and we don't store them.
To run this service separately:
uvicorn x402_service:app --port 8011
Requires:
- A Solana RPC endpoint (for payment verification)
- A USDC wallet for receiving payments
- Separate database from the main WalletPress instance
"""
from __future__ import annotations
import logging
import os
import secrets
import sqlite3
import time
from pathlib import Path
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("x402")
app = FastAPI(title="WalletPress x402 Marketplace", version="1.0.0",
description="Pay-per-wallet generation. No account needed. Send USDC, get keys.")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
# ── Pricing ──────────────────────────────────────────────────────
PRICE_PER_WALLET = 0.01
MIN_ORDER_USD = 1.00
FREE_WALLETS = 3
PAYMENT_ADDRESS = os.getenv("WP_PAYMENT_ADDRESS", "G1uZFfsdxK2PuH2Em5LhRkEcptxHWe4vy5BZak67QyKv")
PAYMENT_CHAIN = "solana"
PAYMENT_TOKEN = "USDC"
# ── Database ─────────────────────────────────────────────────────
DB_PATH = Path(os.getenv("WP_X402_DB", "/data/x402.db"))
def get_db():
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.executescript("""
CREATE TABLE IF NOT EXISTS orders (
order_id TEXT PRIMARY KEY, chain TEXT NOT NULL,
count INTEGER NOT NULL, amount_usd REAL NOT NULL,
payment_tx TEXT DEFAULT '', payment_status TEXT DEFAULT 'pending',
status TEXT DEFAULT 'pending', created_at REAL NOT NULL,
client_ip TEXT DEFAULT '',
idempotency_key TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_orders_idempotency ON orders(idempotency_key);
CREATE TABLE IF NOT EXISTS credits (
api_key TEXT PRIMARY KEY, balance REAL NOT NULL DEFAULT 0,
total_spent REAL DEFAULT 0, created_at REAL NOT NULL
);
""")
conn.commit()
return conn
# ── Models ───────────────────────────────────────────────────────
class GenerateRequest(BaseModel):
chain: str = Field(default="sol", description="Chain key: sol, base, polygon, arbitrum, eth")
count: int = Field(default=1, ge=1, le=100000)
payment_tx: str = Field(default="", description="USDC tx for paid orders. Leave empty for free tier.")
api_key: str = Field(default="", description="Prepaid API key. Alternative to per-order payment.")
derivation_path: str = Field(default="", description="Custom BIP44 derivation path (e.g. m/44'/60'/0'/0/1). Empty = chain default.")
xpub: str = Field(default="", description="Master public key (xpub/tpub/ypub/zpub). Derive child keys without us seeing private keys.")
idempotency_key: str = Field(default="", description="Idempotency key. Same key = same order returned. Prevents double-charge on retry.")
class CreditsRequest(BaseModel):
amount: float = Field(default=10, ge=10)
payment_tx: str = Field(default="")
# ── Helpers ──────────────────────────────────────────────────────
def calc_price(count: int) -> float:
if count >= 100000:
return round(count * 0.001, 2)
if count >= 10000:
return round(count * 0.002, 2)
if count >= 1000:
return round(count * 0.005, 2)
return round(count * PRICE_PER_WALLET, 2)
async def verify_payment(chain: str, tx: str, expected: float) -> bool:
if expected <= 0:
return True
if not tx:
return False
from core.x402_verify import verify_usdc_payment
result = await verify_usdc_payment(chain, tx, expected)
return result["verified"]
# ── Endpoints ────────────────────────────────────────────────────
@app.post("/api/v1/marketplace/generate")
async def generate(req: GenerateRequest, request: Request):
chain = req.chain.lower()
from wallet_engine.chains import CHAINS
if chain not in CHAINS:
raise HTTPException(status_code=400, detail=f"Unsupported chain: {chain}")
count = min(max(req.count, 1), 100000)
total = calc_price(count)
client_ip = request.client.host if request.client else ""
# Idempotency: return existing order if same key used
if req.idempotency_key:
conn = get_db()
existing = conn.execute(
"SELECT * FROM orders WHERE idempotency_key = ?", (req.idempotency_key,)
).fetchone()
conn.close()
if existing:
return {"idempotent": True, "order_id": existing["order_id"], "note": "Order already processed with this idempotency key"}
order_id = f"x402_{secrets.token_hex(8)}"
# Payment via credits
if req.api_key:
conn = get_db()
row = conn.execute("SELECT balance FROM credits WHERE api_key = ?", (req.api_key,)).fetchone()
if not row or row["balance"] < total:
conn.close()
raise HTTPException(status_code=402, detail={
"error": "Insufficient credits", "balance": row["balance"] if row else 0,
"required": total, "buy_url": "/api/v1/marketplace/credits",
})
conn.execute("UPDATE credits SET balance = balance - ?, total_spent = total_spent + ? WHERE api_key = ?",
(total, total, req.api_key))
conn.commit()
conn.close()
payment_method = "credits"
elif total >= MIN_ORDER_USD and not req.payment_tx:
from core.x402_verify import get_pay_to
pay_to = get_pay_to(chain)
raise HTTPException(status_code=402, detail={
"error": "Payment required", "amount_usd": total,
"pay_to": pay_to, "chain": chain, "token": "USDC",
"instructions": f"Send ${total} USDC on {chain.upper()} to {pay_to}",
})
elif total >= MIN_ORDER_USD and req.payment_tx:
if not await verify_payment(chain, req.payment_tx, total):
raise HTTPException(status_code=402, detail="Payment verification failed")
payment_method = "onchain"
else:
payment_method = "free"
# Generate wallets
from wallet_engine.generator import get_generator
generator = get_generator()
wallets = []
if req.xpub:
from bip_utils import Bip32Secp256k1, Bip32Slip10Ed25519
chain_info = CHAINS.get(chain)
if not chain_info:
raise HTTPException(status_code=400, detail=f"Unsupported chain: {chain}")
try:
if chain_info.curve == "ed25519":
bip32_ctx = Bip32Slip10Ed25519.FromXPub(req.xpub)
else:
bip32_ctx = Bip32Secp256k1.FromXPub(req.xpub)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid xpub: {e}")
for i in range(count):
child_path = f"0/{i}"
child_key = bip32_ctx.DerivePath(child_path)
pub_bytes = child_key.PublicKey().Raw().ToBytes()
wallets.append({
"index": i + 1,
"address": pub_bytes.hex()[:40],
"private_key": "",
"mnemonic": "",
"derivation_path": child_path,
"derivation_method": "xpub",
"note": "Derived from your xpub. We never had the private key.",
})
else:
for i in range(count):
gen_kwargs = {
"chain_key": chain,
"label": f"x402_{order_id}_{i}",
"tags": ["x402", order_id],
}
if req.derivation_path:
gen_kwargs["derivation_path"] = req.derivation_path
wallet = generator.generate(**gen_kwargs)
wallets.append({
"index": i + 1, "address": wallet.address,
"private_key": wallet.private_key_hex, "mnemonic": wallet.mnemonic,
"derivation_path": wallet.derivation_path,
})
conn = get_db()
conn.execute(
"INSERT INTO orders (order_id, chain, count, amount_usd, payment_tx, payment_status, status, created_at, client_ip, idempotency_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(order_id, chain, count, total, req.payment_tx or "free", "confirmed", "completed", time.time(), client_ip, req.idempotency_key or ""),
)
conn.commit()
conn.close()
from core.proof import sign_receipt, sign_key_deletion, get_receipt_public_key, get_source_tree_hash
receipt_sig = sign_receipt(order_id, chain, count, total, time.time())
deletion_sig = sign_key_deletion(order_id, chain, count, [w["address"] for w in wallets])
return {
"order_id": order_id, "chain": chain, "count": count,
"total_usd": total, "payment_method": payment_method,
"derivation_method": "xpub" if req.xpub else "mnemonic",
"wallets": wallets,
"receipt": {
"signature": receipt_sig,
"public_key": get_receipt_public_key(),
"verify_url": f"/api/v1/marketplace/verify-receipt?order_id={order_id}",
},
"key_deletion_attestation": {
"signature": deletion_sig,
"algorithm": "Ed25519",
"note": "This proves the private keys were cleared from memory after generation. We did not store them.",
},
"trust": {
"keys_stored": False,
"keys_encrypted": False,
"keys_returned_once": True,
"key_deletion_attested": True,
"open_source": "https://github.com/cryptorugmuncher/walletpress",
"source_commit": get_source_tree_hash(),
"receipt_signed": True,
"receipt_algorithm": "Ed25519",
"audit_logged": True,
"provenance": f"Verify at /api/v1/proof/verify/{order_id}",
"note": "Keys returned once. We do not store them. Signed receipt + key deletion attestation prove authenticity.",
},
}
@app.post("/api/v1/marketplace/credits")
async def buy_credits(req: CreditsRequest):
"""Buy credits via on-chain USDC payment.
SECURITY: Every request with payment_tx MUST be verified on-chain
before crediting. Previously this endpoint accepted any non-empty
payment_tx string and minted credits for free (P0-1).
"""
if not req.payment_tx:
raise HTTPException(status_code=402, detail={
"error": "Payment required", "amount_usd": req.amount,
"pay_to": PAYMENT_ADDRESS, "chain": PAYMENT_CHAIN, "token": "USDC",
})
# Verify the payment on-chain via PayAI facilitator.
# This is the same call used by /generate to validate payment_tx.
verified = await verify_payment(PAYMENT_CHAIN, req.payment_tx, req.amount)
if not verified:
raise HTTPException(status_code=402, detail={
"error": "Payment verification failed",
"payment_tx": req.payment_tx,
"amount_usd": req.amount,
"note": "Send USDC on Solana to the payment address, then retry with the tx signature.",
})
# Only now mint credits
api_key = f"wp_cred_{secrets.token_hex(16)}"
conn = get_db()
conn.execute(
"INSERT INTO credits (api_key, balance, total_spent, created_at) VALUES (?, ?, 0, ?)",
(api_key, req.amount, time.time()),
)
conn.commit()
conn.close()
logger.info(f"x402 credits purchased: amount=${req.amount} tx={req.payment_tx[:16]}... key={api_key[:12]}...")
return {"credits_added": req.amount, "balance": req.amount, "api_key": api_key}
@app.get("/api/v1/marketplace/pricing")
async def pricing():
return {
"per_wallet": PRICE_PER_WALLET,
"minimum_order_usd": MIN_ORDER_USD,
"free_tier": {"wallets": FREE_WALLETS},
"tiers": [
{"range": "1-999", "price": f"${PRICE_PER_WALLET}/wallet"},
{"range": "1,000-9,999", "price": "$0.005/wallet"},
{"range": "10,000-99,999", "price": "$0.002/wallet"},
{"range": "100,000+", "price": "$0.001/wallet"},
],
"payment": {"chain": PAYMENT_CHAIN, "token": PAYMENT_TOKEN, "address": PAYMENT_ADDRESS},
"operator": "Rug Munch Media LLC",
"note": "This is a service operated by WalletPress. Not available for self-hosted installations.",
}
@app.get("/api/v1/marketplace/public-key")
async def marketplace_public_key():
"""Get the server's Ed25519 public key for receipt verification.
Every order receipt is signed with this key. Verify receipts offline
using any Ed25519 library. The key is generated once on first startup
and persisted to disk.
"""
from core.proof import get_receipt_public_key
return {
"algorithm": "Ed25519",
"public_key": get_receipt_public_key(),
"purpose": "Order receipt signing for x402 marketplace",
"generated_at": "first_startup",
"verify_tool": "pip install pynacl && python3 -c \"from nacl.bindings import crypto_sign_open; import base64; ...\"",
}
@app.get("/api/v1/marketplace/verify-receipt")
async def verify_receipt(order_id: str, signature: str = "", pubkey: str = ""):
"""Verify a signed order receipt.
Pass the order_id, signature, and public key from the order response.
Returns whether the signature is valid.
"""
from core.proof import verify_receipt as _verify, get_receipt_public_key
from core.db_pool import DbPool
pool = DbPool(DB_PATH)
row = pool.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone()
if not row:
raise HTTPException(status_code=404, detail="Order not found")
pubkey = pubkey or get_receipt_public_key()
valid = _verify(order_id, row["chain"], row["count"], row["amount_usd"], row["created_at"], signature, pubkey)
return {
"order_id": order_id,
"valid": valid,
"chain": row["chain"],
"count": row["count"],
"amount_usd": row["amount_usd"],
"created_at": row["created_at"],
"public_key_used": pubkey,
"note": "If valid, this receipt proves WalletPress generated these wallets at this time.",
}
if __name__ == "__main__":
import uvicorn
# When run standalone, add health endpoint manually
@app.get("/health")
async def _x402_health():
return {"status": "ok", "service": "x402-marketplace", "version": "1.0.0"}
uvicorn.run("x402_service:app", host="0.0.0.0", port=int(os.getenv("WP_X402_PORT", "8011")))