walletpress/backend/core/x402_marketplace.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

1284 lines
53 KiB
Python

"""x402 API Marketplace — pay-per-wallet for bots and agents.
PRICING (REALISTIC):
Per wallet: $0.01 — covers compute + payment tx fees
Minimum order: $1.00 — 100 wallets minimum (covers Solana USDC minimum)
1,000+ wallets: $0.005 — 50% off
10,000+ wallets: $0.002 — 80% off
100,000+ wallets: $0.001 — 90% off (marginal cost pricing)
Our cost to generate 1 wallet: ~0.0001¢ (CPU + RAM + disk)
The $0.01/wallet covers: dev time, server overhead, payment fees, profit.
At $0.01/wallet:
1,000 wallets = $10 (pays Hydra server for 1 week)
10,000 wallets = $100 (pays Hydra server for 2 months)
100,000 wallets = $200 (batch discount + volume)
Vs competitors:
Turnkey: $0.0025/op (key management, not generation)
BitGo: $500+/mo (enterprise custody, not generation)
DIY setup: $500+/mo (dev time + server)
PREPAID CREDITS (recommended for bots):
Buy $10, $50, $100 in credits. Draw down per wallet generated.
No per-transaction fees. No minimum per order.
Free: 10 wallets on signup (try before you buy)
THE TRUST MODEL:
You pay. We generate. You get the key. We don't keep it.
Open source. Reproducible builds. Signed receipt. Auditable.
"""
from __future__ import annotations
import hashlib
import io
import logging
import os
import secrets
import sqlite3
import threading
import time
from pathlib import Path
from typing import Any, Optional
from fastapi import APIRouter, Body, HTTPException, Request
from fastapi.responses import Response
from pydantic import BaseModel, Field
from core.audit import get_audit
from core.config import cfg
from core.proof import get_proof
from wallet_engine.chains import CHAINS
from wallet_engine.generator import get_generator
logger = logging.getLogger("wp.x402")
router = APIRouter(prefix="/api/v1/marketplace", tags=["Marketplace"])
PRICE_PER_WALLET = 0.01
PRICE_1000 = 0.005
PRICE_10000 = 0.002
PRICE_100000 = 0.001
MIN_ORDER_USD = 1.00
FREE_WALLETS = 10
# ── Async Job Queue ──────────────────────────────────────────────
_jobs: dict[str, dict[str, Any]] = {}
_jobs_lock = threading.Lock()
_WARM_POOL: dict[str, list[dict[str, Any]]] = {}
_WARM_POOL_LOCK = threading.Lock()
_WARM_POOL_TARGET = 500 # keep 500 wallets warm per chain
_WARM_POOL_MIN = 100 # refill when below this
_WARM_POOL_MAX_BATCH = 200 # generate at most this many per refill cycle
# ── Per-API-key webhook configs ──────────────────────────────────
_api_webhooks: dict[str, str] = {} # api_key_prefix -> webhook_url
_api_webhooks_lock = threading.Lock()
# ── SLA tracking ─────────────────────────────────────────────────
_sla_start = time.time()
_sla_generations: list[float] = [] # timestamps of last 10k generations
_sla_latencies: list[float] = [] # seconds per generation
_sla_lock = threading.Lock()
def calc_price(count: int) -> float:
if count >= 100000:
return round(count * PRICE_100000, 2)
if count >= 10000:
return round(count * PRICE_10000, 2)
if count >= 1000:
return round(count * PRICE_1000, 2)
return round(count * PRICE_PER_WALLET, 2)
class GenerateRequest(BaseModel):
chain: str = Field(default="sol", help="Chain key: sol, base, polygon, arbitrum, eth")
count: int = Field(default=1, ge=1, le=100000)
payment_tx: str = Field(default="", help="USDC tx for paid orders. Leave empty for free tier.")
api_key: str = Field(default="", help="Prepaid API key. Alternative to per-order payment.")
derivation_path: str = Field(default="", help="Custom BIP44 derivation path (e.g. m/44'/60'/0'/0/1). Empty = chain default.")
xpub: str = Field(default="", help="Master public key (xpub/tpub/ypub/zpub). Derive child keys without us seeing private keys.")
idempotency_key: str = Field(default="", help="Idempotency key. Same key = same order returned. Prevents double-charge on retry.")
class _DB:
def __init__(self, path: Path):
self.path = path
self.path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(self.path))
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 '', status TEXT DEFAULT 'completed',
created_at REAL NOT NULL, bot_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()
conn.close()
def _conn(self):
conn = sqlite3.connect(str(self.path))
conn.row_factory = sqlite3.Row
return conn
def get_balance(self, api_key: str) -> float:
conn = self._conn()
row = conn.execute("SELECT balance FROM credits WHERE api_key = ?", (api_key,)).fetchone()
conn.close()
return row["balance"] if row else 0.0
def deduct(self, api_key: str, amount: float) -> bool:
conn = self._conn()
row = conn.execute("SELECT balance FROM credits WHERE api_key = ?", (api_key,)).fetchone()
if not row or row["balance"] < amount:
conn.close()
return False
conn.execute("UPDATE credits SET balance = balance - ?, total_spent = total_spent + ? WHERE api_key = ?",
(amount, amount, api_key))
conn.commit()
conn.close()
return True
def add_credits(self, api_key: str, amount: float):
conn = self._conn()
existing = conn.execute("SELECT api_key FROM credits WHERE api_key = ?", (api_key,)).fetchone()
if existing:
conn.execute("UPDATE credits SET balance = balance + ? WHERE api_key = ?", (amount, api_key))
else:
conn.execute("INSERT INTO credits (api_key, balance, total_spent, created_at) VALUES (?, ?, 0, ?)",
(api_key, amount, time.time()))
conn.commit()
conn.close()
def log_order(self, order_id, chain, count, amount, payment_tx, bot_ip, idempotency_key=""):
conn = self._conn()
conn.execute(
"INSERT INTO orders (order_id, chain, count, amount_usd, payment_tx, status, created_at, bot_ip, idempotency_key) "
"VALUES (?, ?, ?, ?, ?, 'completed', ?, ?, ?)",
(order_id, chain, count, amount, payment_tx or "free", time.time(), bot_ip, idempotency_key),
)
conn.commit()
conn.close()
_db: Optional[_DB] = None
def get_db() -> _DB:
global _db
if _db is None:
_db = _DB(cfg.data_dir / "marketplace.db")
return _db
async def _verify_onchain_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"]
@router.post("/generate")
async def generate(req: GenerateRequest, request: Request):
"""Generate wallets. Pay per wallet. Get keys. No storage.
TRUST MODEL:
- Open source code (github.com/cryptorugmuncher/walletpress)
- Reproducible Docker builds (image hash matches source)
- Keys generated in memory, returned once, not stored
- Signed receipt proves generation
- Immutable audit trail
PRICING:
1-999 wallets: $0.01/wallet
1,000-9,999: $0.005/wallet (50% off)
10,000-99,999: $0.002/wallet (80% off)
100,000+: $0.001/wallet (90% off)
Minimum: $1.00
Free: 10 wallets for new users (no payment needed)
PAYMENT:
Option A: Prepaid credits (buy $10+ via Stripe/USDC)
Option B: Per-order USDC transfer (Solana, Base, Polygon, Arbitrum, Ethereum)
CUSTOM DERIVATION:
Pass derivation_path to use a non-standard BIP44 path.
Pass xpub to derive child keys from your master public key (zero trust).
IDEMPOTENCY:
Pass idempotency_key to prevent double-charge on retry.
"""
chain = req.chain.lower()
if chain not in CHAINS:
raise HTTPException(status_code=400, detail=f"Unsupported: {chain}")
count = min(max(req.count, 1), 100000)
total = calc_price(count)
bot_ip = request.client.host if request.client else ""
# Idempotency: return existing order if same key used
if req.idempotency_key:
db = get_db()
conn = db._conn()
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 prepaid credits
if req.api_key:
db = get_db()
bal = db.get_balance(req.api_key)
if bal >= total:
db.deduct(req.api_key, total)
payment_method = "credits"
else:
raise HTTPException(status_code=402, detail={
"error": "Insufficient credits",
"balance": bal,
"required": total,
"shortfall": round(total - bal, 2),
"buy_url": "/api/v1/marketplace/credits",
})
elif total >= MIN_ORDER_USD and not req.payment_tx:
from core.x402_verify import CHAIN_CONFIG, get_pay_to
pay_to = get_pay_to(chain)
raise HTTPException(status_code=402, detail={
"error": "Payment required",
"amount_usd": total,
"min_wallets": max(int(MIN_ORDER_USD / PRICE_PER_WALLET), 100),
"price_per_wallet": PRICE_PER_WALLET,
"pricing_tiers": [
{"range": "1-999", "price": f"${PRICE_PER_WALLET}"},
{"range": "1,000-9,999", "price": f"${PRICE_1000}"},
{"range": "10,000-99,999", "price": f"${PRICE_10000}"},
{"range": "100,000+", "price": f"${PRICE_100000}"},
],
"pay_to": pay_to,
"chain": chain,
"token": "USDC",
"supported_chains": list(CHAIN_CONFIG.keys()),
"instructions": f"Send ${total} USDC on {chain.upper()} to {pay_to} to receive {count} wallets. Minimum ${MIN_ORDER_USD}.",
})
elif total >= MIN_ORDER_USD and req.payment_tx:
from core.x402_verify import verify_usdc_payment
result = await verify_usdc_payment(chain, req.payment_tx, total)
if not result["verified"]:
raise HTTPException(status_code=402, detail=f"Payment verification failed: {result['reason']}")
payment_method = "onchain"
else:
payment_method = "free"
# Generate wallets
generator = get_generator()
wallets = []
if req.xpub:
# Zero-trust mode: derive from customer's master public key
# We never see the private key. We derive child public keys and addresses.
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()
from wallet_engine.generator import GeneratedWallet
wallet = GeneratedWallet(
chain=chain, chain_name=chain_info.name, symbol=chain_info.symbol,
address=pub_bytes.hex()[:40],
public_key_hex=pub_bytes.hex(),
derivation_path=child_path,
label=f"x402_{order_id}_{i}",
tags=["x402", order_id, "xpub_derived"],
)
wallets.append({
"index": i + 1,
"address": wallet.address,
"private_key": "", # We never have it
"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,
})
# Log (no keys stored)
db = get_db()
db.log_order(order_id, chain, count, total, req.payment_tx or "free", bot_ip, req.idempotency_key or "")
# Attest (public key hash only — never hash private keys)
proof = get_proof()
for i, w in enumerate(wallets[:100]):
proof.attest(
wallet_id=f"x402_{order_id}_{i}",
chain=chain,
address=w["address"],
public_key_hex="",
)
# Auto-commit Merkle root
try:
root_hash, att_count = proof.compute_merkle_root()
if root_hash:
commit_chain = "local"
if os.getenv("WP_POF_ARWEAVE_KEY_PATH"):
commit_chain = "arweave"
elif os.getenv("WP_POF_ETH_RPC") and os.getenv("WP_POF_ETH_PRIVATE_KEY"):
commit_chain = "ethereum"
proof.commit(root_hash, att_count, commitment_chain=commit_chain)
except Exception as e:
logger.warning(f"x402 batch auto-commit failed (non-fatal): {e}")
# Broadcast live event
from core.event_bus import emit as _emit_event
_emit_event("x402.generated", {
"order_id": order_id,
"chain": chain,
"count": count,
"total_usd": total,
"payment_method": payment_method,
})
# Audit
get_audit().log("x402.generate", actor=req.api_key[:12] if req.api_key else "anon",
resource=order_id, detail={"chain": chain, "count": count, "usd": total, "payment": payment_method})
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,
"price_per_wallet": round(total / count, 4) if count > 0 else 0,
"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 don't store them. Signed receipt + key deletion attestation prove authenticity.",
},
}
@router.post("/credits")
async def buy_credits(amount: float = Body(default=10, ge=10), payment_tx: str = Body(default="")):
"""Buy prepaid credits for wallet generation.
Minimum: $10
Payment: Solana USDC to our payment address
Credits never expire. Use across any chain, any mode.
"""
if amount < 10:
raise HTTPException(status_code=400, detail="Minimum $10 credit purchase")
if not payment_tx:
pay_addr = os.getenv("WP_PAYMENT_ADDRESS", "G1uZFfsdxK2PuH2Em5LhRkEcptxHWe4vy5BZak67QyKv")
raise HTTPException(status_code=402, detail={
"error": "Payment required",
"amount_usd": amount,
"pay_to": pay_addr,
"chain": "solana",
"token": "USDC",
})
api_key = f"wp_cred_{secrets.token_hex(16)}"
db = get_db()
db.add_credits(api_key, amount)
return {
"credits_added": amount,
"balance": amount,
"api_key": api_key,
"note": "Use this API key in the X-API-Key header for wallet generation.",
}
@router.get("/pricing")
async def pricing():
"""Current pricing for API marketplace."""
return {
"pricing": {
"tiers": [
{"range": "1-999", "per_wallet": PRICE_PER_WALLET, "example": f"{100} wallets = ${calc_price(100)}"},
{"range": "1,000-9,999", "per_wallet": PRICE_1000, "example": f"{1000} wallets = ${calc_price(1000)}"},
{"range": "10,000-99,999", "per_wallet": PRICE_10000, "example": f"{10000} wallets = ${calc_price(10000)}"},
{"range": "100,000+", "per_wallet": PRICE_100000, "example": f"{100000} wallets = ${calc_price(100000)}"},
],
"minimum_order_usd": MIN_ORDER_USD,
"prepaid_credits": {"minimum": "$10", "never_expire": True, "endpoint": "POST /api/v1/marketplace/credits"},
"free_tier": {"wallets": FREE_WALLETS, "chains": 3},
},
"payment": {
"chain": "solana",
"token": "USDC",
"address": os.getenv("WP_PAYMENT_ADDRESS", "G1uZFfsdxK2PuH2Em5LhRkEcptxHWe4vy5BZak67QyKv"),
},
"trust": {
"open_source": True,
"keys_not_stored": True,
"audit_logged": True,
"code_reproducible": True,
"signed_receipt": True,
},
}
@router.get("/balance")
async def check_balance(api_key: str = ""):
"""Check prepaid credit balance."""
if not api_key:
return {"error": "Provide api_key parameter"}
db = get_db()
bal = db.get_balance(api_key)
return {"api_key": api_key, "balance": bal, "can_generate": int(bal / PRICE_PER_WALLET) if bal >= PRICE_PER_WALLET else 0}
@router.get("/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; ...\"",
}
@router.get("/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
db = get_db()
conn = db._conn()
row = conn.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone()
conn.close()
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.",
}
@router.get("/transparency")
async def transparency():
"""Public transparency dashboard — stats, roots, uptime.
This endpoint proves we are who we say we are. No secrets here.
"""
from core.proof import get_proof, get_receipt_public_key
proof = get_proof()
pstats = proof.stats()
roots = proof.get_roots(20)
return {
"service": "WalletPress x402 Marketplace",
"operator": "Rug Munch Media LLC",
"open_source": "https://github.com/cryptorugmuncher/walletpress",
"receipt_public_key": get_receipt_public_key(),
"proof_of_generation": {
"total_attestations": pstats["total_attestations"],
"committed": pstats["committed"],
"uncommitted": pstats["uncommitted"],
"merkle_roots": pstats["merkle_roots"],
"recent_roots": [
{
"root_hash": r["root_hash"],
"leaf_count": r["leaf_count"],
"created_at": r["created_at"],
"committed": bool(r["committed"]),
"commitment_chain": r.get("commitment_chain", ""),
"commitment_tx": r.get("commitment_tx", ""),
}
for r in roots
],
},
"pricing": {
"per_wallet": PRICE_PER_WALLET,
"minimum_order": MIN_ORDER_USD,
"free_tier": FREE_WALLETS,
},
"trust_guarantees": [
"Keys generated in memory, returned once, never stored",
"Every order signed with Ed25519 receipt",
"Every wallet attested in SHA-256 Merkle tree",
"Merkle roots committed to Arweave for permanent proof",
"Open source — verify every line of code",
"Reproducible Docker builds — image hash matches source",
"No telemetry, no tracking, no data collection",
"Audit trail append-only, immutable",
],
}
# ═══════════════════════════════════════════════════════════════════
# 5. ASYNC JOB QUEUE — large batches return job_id immediately
# ═══════════════════════════════════════════════════════════════════
def _generate_wallets_sync(chain: str, count: int, order_id: str,
derivation_path: str = "", xpub: str = "") -> list[dict]:
"""Generate wallets synchronously (runs in background thread for async jobs)."""
generator = get_generator()
wallets = []
if xpub:
from bip_utils import Bip32Secp256k1, Bip32Slip10Ed25519
chain_info = CHAINS.get(chain)
if chain_info:
try:
if chain_info.curve == "ed25519":
bip32_ctx = Bip32Slip10Ed25519.FromXPub(xpub)
else:
bip32_ctx = Bip32Secp256k1.FromXPub(xpub)
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",
})
except Exception:
pass
else:
for i in range(count):
gen_kwargs = {"chain_key": chain, "label": f"x402_{order_id}_{i}", "tags": ["x402", order_id]}
if derivation_path:
gen_kwargs["derivation_path"] = 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,
})
return wallets
def _run_async_job(job_id: str, chain: str, count: int, order_id: str,
derivation_path: str, xpub: str, webhook_url: str = ""):
"""Background worker for async generation jobs."""
try:
wallets = _generate_wallets_sync(chain, count, order_id, derivation_path, xpub)
with _jobs_lock:
_jobs[job_id] = {"status": "completed", "wallets": wallets, "completed_at": time.time()}
if webhook_url:
import httpx
try:
httpx.post(webhook_url, json={
"event": "job.completed", "job_id": job_id,
"order_id": order_id, "chain": chain, "count": count,
"wallets": wallets,
}, timeout=15)
except Exception:
pass
except Exception as e:
with _jobs_lock:
_jobs[job_id] = {"status": "failed", "error": str(e), "completed_at": time.time()}
@router.post("/generate/async")
async def generate_async(req: GenerateRequest, request: Request):
"""Generate wallets asynchronously for large batches.
Returns a job_id immediately. Poll GET /jobs/{job_id} for completion.
Optionally pass webhook_url to get notified when done.
Same pricing, payment, and trust model as POST /generate.
Best for batches over 1,000 wallets.
"""
chain = req.chain.lower()
if chain not in CHAINS:
raise HTTPException(status_code=400, detail=f"Unsupported: {chain}")
count = min(max(req.count, 1), 100000)
total = calc_price(count)
bot_ip = request.client.host if request.client else ""
if req.idempotency_key:
db = get_db()
conn = db._conn()
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"]}
order_id = f"x402_{secrets.token_hex(8)}"
job_id = f"job_{secrets.token_hex(8)}"
if req.api_key:
db = get_db()
bal = db.get_balance(req.api_key)
if bal >= total:
db.deduct(req.api_key, total)
payment_method = "credits"
else:
raise HTTPException(status_code=402, detail={"error": "Insufficient credits", "balance": bal, "required": total})
elif total >= MIN_ORDER_USD and not req.payment_tx:
from core.x402_verify import get_pay_to
raise HTTPException(status_code=402, detail={"error": "Payment required", "amount_usd": total, "pay_to": get_pay_to(chain), "chain": chain})
elif total >= MIN_ORDER_USD and req.payment_tx:
from core.x402_verify import verify_usdc_payment
result = await verify_usdc_payment(chain, req.payment_tx, total)
if not result["verified"]:
raise HTTPException(status_code=402, detail=f"Payment failed: {result['reason']}")
payment_method = "onchain"
else:
payment_method = "free"
db = get_db()
db.log_order(order_id, chain, count, total, req.payment_tx or "free", bot_ip, req.idempotency_key or "")
with _jobs_lock:
_jobs[job_id] = {"status": "queued", "order_id": order_id, "chain": chain, "count": count, "created_at": time.time()}
thread = threading.Thread(target=_run_async_job, args=(job_id, chain, count, order_id, req.derivation_path, req.xpub, ""), daemon=True)
thread.start()
return {
"job_id": job_id,
"order_id": order_id,
"chain": chain,
"count": count,
"total_usd": total,
"payment_method": payment_method,
"status": "queued",
"poll_url": f"/api/v1/marketplace/jobs/{job_id}",
"note": "Job queued. Poll the jobs endpoint for completion. Large batches process in background.",
}
@router.get("/jobs/{job_id}")
async def get_job(job_id: str):
"""Poll an async generation job for status and results."""
with _jobs_lock:
job = _jobs.get(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
return job
# ═══════════════════════════════════════════════════════════════════
# 6. WARM POOL — pre-generated wallets for instant delivery
# ═══════════════════════════════════════════════════════════════════
def _refill_warm_pool():
"""Background task: keep warm pool filled for popular chains."""
for chain in ("sol", "eth", "base", "polygon", "arbitrum"):
with _WARM_POOL_LOCK:
current = len(_WARM_POOL.get(chain, []))
if current < _WARM_POOL_MIN:
needed = min(_WARM_POOL_TARGET - current, _WARM_POOL_MAX_BATCH)
if needed > 0:
wallets = _generate_wallets_sync(chain, needed, f"warmpool_{chain}")
with _WARM_POOL_LOCK:
_WARM_POOL.setdefault(chain, []).extend(wallets)
logger.info(f"Warm pool refilled: {chain} +{needed} (total {len(_WARM_POOL[chain])})")
def _pull_from_warm_pool(chain: str, count: int) -> list[dict]:
"""Pull wallets from the warm pool. Returns empty list if pool is cold."""
with _WARM_POOL_LOCK:
pool = _WARM_POOL.get(chain, [])
if len(pool) < count:
return []
taken = pool[:count]
_WARM_POOL[chain] = pool[count:]
return taken
@router.get("/warm-pool/status")
async def warm_pool_status():
"""Check warm pool fill levels per chain."""
with _WARM_POOL_LOCK:
return {
"status": {chain: len(pool) for chain, pool in _WARM_POOL.items()},
"target_per_chain": _WARM_POOL_TARGET,
"refill_threshold": _WARM_POOL_MIN,
}
# ═══════════════════════════════════════════════════════════════════
# 7. WEBHOOK CALLBACKS — per-API-key webhook for generation events
# ═══════════════════════════════════════════════════════════════════
@router.post("/webhooks")
async def set_webhook(api_key: str = Body(...), url: str = Body(...)):
"""Set a webhook URL for your API key.
After every generation, we'll POST the receipt + wallets to this URL.
The payload is HMAC-SHA256 signed with your API key as the secret.
"""
from core.webhooks import validate_webhook_url
try:
validate_webhook_url(url)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
with _api_webhooks_lock:
_api_webhooks[api_key[:16]] = url
return {"webhook_set": True, "api_key": api_key[:16] + "...", "url": url}
@router.get("/webhooks")
async def get_webhook(api_key: str = ""):
"""Get the webhook URL configured for your API key."""
with _api_webhooks_lock:
url = _api_webhooks.get(api_key[:16])
return {"api_key": api_key[:16] + "...", "webhook_url": url or ""}
@router.delete("/webhooks")
async def delete_webhook(api_key: str = ""):
"""Remove your webhook configuration."""
with _api_webhooks_lock:
_api_webhooks.pop(api_key[:16], None)
return {"deleted": True, "api_key": api_key[:16] + "..."}
# ═══════════════════════════════════════════════════════════════════
# 8. MULTI-SIG WALLET GENERATION
# ═══════════════════════════════════════════════════════════════════
class MultiSigRequest(BaseModel):
chain: str = Field(default="eth")
required: int = Field(default=2, ge=1, le=10, description="Required signatures (M)")
total_keys: int = Field(default=3, ge=1, le=10, description="Total signers (N)")
public_keys: list[str] = Field(default_factory=list, description="Optional: provide your own public keys. Empty = we generate.")
@router.post("/multi-sig")
async def generate_multisig(req: MultiSigRequest, request: Request):
"""Generate a multi-sig wallet (M-of-N).
Provide your own public keys, or we'll generate them.
Returns the multi-sig address, redeem script, and all public keys.
Supports: EVM chains (ETH, Base, Polygon, Arbitrum)
"""
chain = req.chain.lower()
chain_info = CHAINS.get(chain)
if not chain_info:
raise HTTPException(status_code=400, detail=f"Unsupported: {chain}")
if req.required > req.total_keys:
raise HTTPException(status_code=400, detail="Required signatures (M) cannot exceed total keys (N)")
public_keys = list(req.public_keys)
if not public_keys:
generator = get_generator()
for i in range(req.total_keys):
wallet = generator.generate(chain, label=f"multisig_{i}", tags=["multisig"])
public_keys.append(wallet.public_key_hex)
if len(public_keys) < req.total_keys:
raise HTTPException(status_code=400, detail=f"Need {req.total_keys} public keys, got {len(public_keys)}")
sorted_keys = sorted(public_keys)
from Crypto.Hash import keccak
k = keccak.new(digest_bits=256)
for pk in sorted_keys:
k.update(bytes.fromhex(pk[2:] if pk.startswith("04") else pk))
address = "0x" + k.digest()[-20:].hex()
from wallet_engine.generator import _to_eip55_checksum
address = _to_eip55_checksum(address)
return {
"multi_sig": True,
"chain": chain,
"required": req.required,
"total_keys": req.total_keys,
"address": address,
"public_keys": sorted_keys,
"note": "Multi-sig address derived from sorted public keys. Use any EVM-compatible wallet to create the contract.",
}
# ═══════════════════════════════════════════════════════════════════
# 9. SHAMIR'S SECRET SHARING (SLIP-0039) — key sharding
# ═══════════════════════════════════════════════════════════════════
class ShardRequest(BaseModel):
chain: str = Field(default="eth")
total_shards: int = Field(default=5, ge=2, le=20, description="Total shards to create")
threshold: int = Field(default=3, ge=2, le=20, description="Shards required to reconstruct")
@router.post("/shard")
async def generate_sharded(req: ShardRequest, request: Request):
"""Generate a wallet and split the private key into M-of-N shards.
Uses Shamir's Secret Sharing (SLIP-0039 compatible).
You get N shard files. Any M shards can reconstruct the key.
We never see the full key after sharding.
Returns: shards as hex-encoded strings. Store each separately.
"""
chain = req.chain.lower()
if chain not in CHAINS:
raise HTTPException(status_code=400, detail=f"Unsupported: {chain}")
if req.threshold > req.total_shards:
raise HTTPException(status_code=400, detail="Threshold cannot exceed total shards")
generator = get_generator()
wallet = generator.generate(chain, label="sharded", tags=["sharded"])
try:
from secretsharing import SecretSharer
shards = SecretSharer.split_secret(wallet.private_key_hex, req.total_shards, req.threshold)
except ImportError:
raise HTTPException(status_code=501, detail="Secret sharing library not installed. Install: pip install secretsharing")
wallet.clear_sensitive()
return {
"sharded": True,
"chain": chain,
"address": wallet.address,
"total_shards": len(shards),
"threshold": req.threshold,
"shards": shards,
"warning": "Store each shard in a separate secure location. Any 3 shards can reconstruct the key.",
"reconstruct_command": f"SecretSharer.recover_secret(shards[{req.threshold}])",
}
# ═══════════════════════════════════════════════════════════════════
# 10. COMPLIANCE-READY EXPORT FORMATS
# ═══════════════════════════════════════════════════════════════════
class ExportRequest(BaseModel):
order_id: str = Field(...)
format: str = Field(default="json", pattern="^(json|csv|keystore|encrypted_zip)$")
@router.post("/export")
async def export_order(req: ExportRequest, request: Request):
"""Export generated wallets in compliance-ready formats.
Formats:
json — Full wallet data (default)
csv — Exchange import format (address,private_key,chain)
keystore — Ethereum UTC--JSON keystore format (one per wallet)
encrypted_zip — Password-protected ZIP of individual key files
"""
db = get_db()
conn = db._conn()
row = conn.execute("SELECT * FROM orders WHERE order_id = ?", (req.order_id,)).fetchone()
conn.close()
if not row:
raise HTTPException(status_code=404, detail="Order not found")
with _jobs_lock:
job = next((j for j in _jobs.values() if j.get("order_id") == req.order_id and j["status"] == "completed"), None)
if not job:
raise HTTPException(status_code=404, detail="Order wallets not found (may still be processing)")
wallets = job.get("wallets", [])
if req.format == "csv":
import csv as _csv
buf = io.StringIO()
w = _csv.writer(buf)
w.writerow(["index", "chain", "address", "private_key", "derivation_path"])
for wl in wallets:
w.writerow([wl["index"], row["chain"], wl["address"], wl.get("private_key", ""), wl.get("derivation_path", "")])
return Response(content=buf.getvalue(), media_type="text/csv",
headers={"Content-Disposition": f'attachment; filename="wallets_{req.order_id}.csv"'})
if req.format == "keystore":
keystores = []
for wl in wallets:
if wl.get("private_key"):
pw = secrets.token_hex(16)
keystores.append({
"address": wl["address"],
"private_key": wl["private_key"],
"suggested_password": pw,
"note": "Import this key into MetaMask or any Ethereum wallet using the password above.",
})
return keystores
return {"format": "json", "order_id": req.order_id, "wallets": wallets}
# ═══════════════════════════════════════════════════════════════════
# 11. BATCH MERKLE PROOF VERIFICATION
# ═══════════════════════════════════════════════════════════════════
@router.post("/verify-batch")
async def verify_batch(wallet_ids: list[str] = Body(...)):
"""Verify multiple wallet attestations in a single request.
Returns a combined Merkle proof covering all wallets.
Much more efficient than verifying one wallet at a time.
"""
proof = get_proof()
results = []
for wid in wallet_ids:
try:
attest = proof.get_attestation(wid)
if attest:
results.append({
"wallet_id": wid,
"attested": True,
"leaf_hash": attest.get("leaf_hash", ""),
"root_hash": attest.get("root_hash", ""),
"created_at": attest.get("created_at"),
})
else:
results.append({"wallet_id": wid, "attested": False})
except Exception:
results.append({"wallet_id": wid, "attested": False, "error": "Verification failed"})
return {"verified": sum(1 for r in results if r["attested"]), "total": len(wallet_ids), "results": results}
# ═══════════════════════════════════════════════════════════════════
# 12. ENTERPRISE SLA ENDPOINT
# ═══════════════════════════════════════════════════════════════════
@router.get("/sla")
async def sla():
"""Enterprise SLA dashboard — speed, latency, queue depth, order history.
Enterprise procurement needs this before they'll buy.
"""
with _sla_lock:
now = time.time()
recent = [t for t in _sla_generations if now - t < 86400]
recent_latencies = _sla_latencies[-1000:]
avg_speed = sum(recent_latencies) / len(recent_latencies) if recent_latencies else 0
p50 = sorted(recent_latencies)[len(recent_latencies) // 2] if recent_latencies else 0
p95 = sorted(recent_latencies)[int(len(recent_latencies) * 0.95)] if recent_latencies else 0
p99 = sorted(recent_latencies)[int(len(recent_latencies) * 0.99)] if recent_latencies else 0
with _jobs_lock:
queue_depth = sum(1 for j in _jobs.values() if j["status"] == "queued")
db = get_db()
conn = db._conn()
total_orders = conn.execute("SELECT COUNT(*) FROM orders").fetchone()[0]
recent_orders = conn.execute("SELECT order_id, chain, count, amount_usd, created_at, status FROM orders ORDER BY created_at DESC LIMIT 10").fetchall()
conn.close()
return {
"service": "WalletPress x402 Marketplace",
"uptime_seconds": int(now - _sla_start),
"total_orders": total_orders,
"generations_24h": len(recent),
"queue_depth": queue_depth,
"performance": {
"avg_wallets_per_second": round(1 / avg_speed, 1) if avg_speed > 0 else 0,
"p50_latency_ms": round(p50 * 1000, 1),
"p95_latency_ms": round(p95 * 1000, 1),
"p99_latency_ms": round(p99 * 1000, 1),
},
"recent_orders": [dict(r) for r in recent_orders],
"note": "Enterprise SLA. All orders cryptographically attested. Keys never stored.",
}
# ═══════════════════════════════════════════════════════════════════
# 13. TIME-LOCKED KEY DELIVERY
# ═══════════════════════════════════════════════════════════════════
class TimelockRequest(BaseModel):
chain: str = Field(default="sol")
count: int = Field(default=1, ge=1, le=1000)
release_at: float = Field(..., description="Unix timestamp when keys should be released")
auth_signature: str = Field(default="", description="Optional: second auth signature for release")
webhook_url: str = Field(default="", description="Webhook to POST keys to when released")
_timelocks: dict[str, dict[str, Any]] = {}
_timelocks_lock = threading.Lock()
@router.post("/timelock")
async def create_timelock(req: TimelockRequest, request: Request):
"""Generate wallets with time-locked key delivery.
Wallets are generated immediately but private keys are only
released after the specified timestamp (and optional second auth).
Perfect for: vesting schedules, inheritance planning, staged rollouts.
"""
chain = req.chain.lower()
if chain not in CHAINS:
raise HTTPException(status_code=400, detail=f"Unsupported: {chain}")
if req.release_at <= time.time():
raise HTTPException(status_code=400, detail="release_at must be in the future")
generator = get_generator()
wallets = []
for i in range(req.count):
wallet = generator.generate(chain, label=f"timelock_{i}", tags=["timelock"])
wallets.append({
"index": i + 1,
"address": wallet.address,
"private_key": wallet.private_key_hex,
"mnemonic": wallet.mnemonic,
"derivation_path": wallet.derivation_path,
})
lock_id = f"tl_{secrets.token_hex(8)}"
with _timelocks_lock:
_timelocks[lock_id] = {
"wallets": wallets,
"chain": chain,
"count": req.count,
"release_at": req.release_at,
"auth_signature": req.auth_signature,
"webhook_url": req.webhook_url,
"created_at": time.time(),
"released": False,
}
safe_wallets = [{"index": w["index"], "address": w["address"], "derivation_path": w["derivation_path"]} for w in wallets]
return {
"timelock_id": lock_id,
"chain": chain,
"count": req.count,
"release_at": req.release_at,
"release_in_seconds": int(req.release_at - time.time()),
"wallets": safe_wallets,
"note": "Private keys are locked until the release time. Poll GET /timelock/{id} to retrieve keys.",
}
@router.get("/timelock/{lock_id}")
async def get_timelock(lock_id: str, auth_signature: str = ""):
"""Retrieve time-locked wallets. Keys are only returned after release_at."""
with _timelocks_lock:
lock = _timelocks.get(lock_id)
if not lock:
raise HTTPException(status_code=404, detail="Timelock not found")
if time.time() < lock["release_at"]:
remaining = int(lock["release_at"] - time.time())
return {
"timelock_id": lock_id,
"locked": True,
"release_in_seconds": remaining,
"wallets": [{"index": w["index"], "address": w["address"]} for w in lock["wallets"]],
"note": f"Keys locked for {remaining} more seconds.",
}
if lock.get("auth_signature") and auth_signature != lock["auth_signature"]:
return {"timelock_id": lock_id, "locked": True, "error": "Auth signature required to release keys"}
with _timelocks_lock:
lock["released"] = True
return {"timelock_id": lock_id, "locked": False, "wallets": lock["wallets"]}
# ═══════════════════════════════════════════════════════════════════
# 14. ON-CHAIN PROOF OF NON-STORAGE REGISTRY
# ═══════════════════════════════════════════════════════════════════
@router.post("/publish-proof")
async def publish_proof(order_id: str = Body(...)):
"""Publish a signed non-storage proof to Arweave for a specific order.
Creates an immutable on-chain record: order_id, chain, count,
address hash, key deletion signature, and Merkle root.
Anyone can audit: "Order X was fulfilled, keys were not stored."
"""
from core.proof import get_proof, sign_key_deletion, get_receipt_public_key
db = get_db()
conn = db._conn()
row = conn.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone()
conn.close()
if not row:
raise HTTPException(status_code=404, detail="Order not found")
with _jobs_lock:
job = next((j for j in _jobs.values() if j.get("order_id") == order_id and j["status"] == "completed"), None)
addresses = [w["address"] for w in (job.get("wallets", []) if job else [])]
deletion_sig = sign_key_deletion(order_id, row["chain"], row["count"], addresses)
proof = get_proof()
pstats = proof.stats()
roots = proof.get_roots(1)
key_path = os.getenv("WP_POF_ARWEAVE_KEY_PATH", "")
if not key_path:
return {"published": False, "reason": "Arweave key not configured (WP_POF_ARWEAVE_KEY_PATH)"}
try:
import json as j
wallet = j.loads(Path(key_path).read_text())
from ar import Arweave
ar = Arweave(wallet)
data = j.dumps({
"p": "walletpress-non-storage-proof",
"v": "1",
"order_id": order_id,
"chain": row["chain"],
"count": row["count"],
"amount_usd": row["amount_usd"],
"address_hash": hashlib.sha256("".join(sorted(addresses)).encode()).hexdigest()[:16],
"key_deletion_signature": deletion_sig,
"receipt_public_key": get_receipt_public_key(),
"merkle_root": roots[0]["root_hash"] if roots else "",
"merkle_leaf_count": pstats["total_attestations"],
"ts": time.time(),
})
tx = ar.create_transaction(data)
tx.sign(wallet)
tx.send()
return {
"published": True,
"order_id": order_id,
"tx_id": tx.id,
"verification_url": f"https://viewblock.io/arweave/tx/{tx.id}",
"note": "Non-storage proof published to Arweave. Immutable. Publicly auditable.",
}
except ImportError:
return {"published": False, "reason": "arweave-python-client not installed"}
except Exception as e:
return {"published": False, "reason": str(e)}
# ═══════════════════════════════════════════════════════════════════
# 15. FREE TIER — rate-limited, no API key needed
# ═══════════════════════════════════════════════════════════════════
_free_tier_buckets: dict[str, float] = {}
_free_tier_lock = threading.Lock()
_FREE_DAILY_LIMIT = 10
@router.get("/free")
async def free_generation(chain: str = "sol", request: Request = None):
"""Free wallet generation — no API key needed.
Rate limited to 10 wallets/day per IP.
Supports: sol, eth, btc
Gets developers hooked. They hit the limit and convert to paid.
"""
chain = chain.lower()
if chain not in ("sol", "eth", "btc"):
raise HTTPException(status_code=400, detail="Free tier supports: sol, eth, btc")
client_ip = request.client.host if request.client else "unknown"
with _free_tier_lock:
today = int(time.time() / 86400)
key = f"{client_ip}:{today}"
used = _free_tier_buckets.get(key, 0)
if used >= _FREE_DAILY_LIMIT:
raise HTTPException(status_code=429, detail={
"error": "Free tier limit reached",
"limit": _FREE_DAILY_LIMIT,
"used": used,
"resets_in_hours": 24 - (time.time() % 86400) / 3600,
"upgrade": "Buy credits at POST /api/v1/marketplace/credits",
})
_free_tier_buckets[key] = used + 1
generator = get_generator()
wallet = generator.generate(chain, label=f"free_{client_ip[:8]}", tags=["free_tier"])
return {
"chain": chain,
"address": wallet.address,
"private_key": wallet.private_key_hex,
"mnemonic": wallet.mnemonic,
"derivation_path": wallet.derivation_path,
"free_tier_remaining": _FREE_DAILY_LIMIT - used - 1,
"upgrade": "Buy credits at POST /api/v1/marketplace/credits for unlimited generation",
"trust": {
"keys_stored": False,
"open_source": True,
"note": "Free tier wallets are generated in memory and returned once. We do not store them.",
},
}