HIP-18: Payment Processing Standard. Status Draft. Hanzo's own standard — read this before implementing against it.
This proposal defines the payment processing standard for the Hanzo ecosystem. Hanzo Commerce is the billing, subscription, and payment service that bridges the native Hanzo PSP (Hanzo Pay — the same payment engine white-labeled as lux-pay on Lux and per-brand elsewhere) with the internal credit system managed by Hanzo IAM (HIP-26). Card data is tokenized inside the Hanzo Vault PCI CDE; on-chain rails settle $AI directly. Every dollar a user pays is converted into credits. Every AI inference, API call, or compute job consumes credits. Commerce handles the money side; IAM holds the balance; the LLM Gateway (HIP-4) and Cloud services meter usage.
No external-processor dependency. First-party Hanzo/Lux/Zoo/Zen surfaces process payments through the native PSP, never through a third-party processor that can deplatform us. Commerce keeps a pluggable provider-adapter interface so a brand MAY add a regional rail, but no first-party flow depends on one.
The system is designed around a single invariant: IAM is the source of truth for user balances. Commerce writes credits in; Cloud and Gateway write credits out. No service other than IAM may directly mutate a user's balance. All mutations flow through IAM's transaction API.
Repository: github.com/hanzoai/commerce Port: 4242 Docker: ghcr.io/hanzoai/commerce:latest
The native PSP (Hanzo Pay, the same engine white-labeled as lux-pay / per-brand) provides:
The compliance burden the standard once cited as a reason to outsource (PCI DSS: 300+ controls across 12 categories) is met by scoping the CDE to Hanzo Vault alone, not by handing the customer relationship to a party that can revoke it.
Commerce keeps a provider-adapter interface so a brand MAY plug an additional rail (regional cards, bank transfer, a Merchant-of-Record for EU VAT, etc.). These are optional, per-brand, and never on a first-party critical path — the native PSP is always the default and the only rail a first-party surface is required to support. Adapters are registered explicitly; none is implicit or default. (Notably, no first-party surface registers an external processor that can deplatform the ecosystem.)
The Hanzo ecosystem includes $AI token (HIP-1) and on-chain settlement (HIP-25). Why not use blockchain for all payments?
The correct approach is both: the native PSP for fiat, blockchain for crypto. Commerce accepts both and normalizes them into credits. The user does not need to know or care which payment rail was used.
+------------------------+
| Hanzo Pay (native PSP)|
| + Hanzo Vault (PCI |
| CDE: card tokens) |
| Checkout/Billing/Hooks|
+------+------+----------+
| |
webhooks | | checkout sessions
| |
+------v------v----------+
| Hanzo Commerce |
| (payment logic) |
| :4242 |
+------+--------+--------+
| |
+--------------+--------+-------------+
| | |
add-balance add-transaction get-account
| | |
v v v
+----------------------------------------------+
| Hanzo IAM (HIP-26) |
| (user balances, transactions) |
| hanzo.id :8000 |
+----------------------------------------------+
^ ^
| |
token validation debit transactions
| |
+------------+-+ +------+----------+
| LLM Gateway | | Hanzo Cloud |
| (HIP-4) | | (compute jobs) |
| :4000 | | |
+--------------+ +-----------------+
| Property | Value | |----------|-------| | Unit name | credit | | USD value | 1 credit = $0.001 USD | | Minimum purchase | 1,000 credits ($1.00) | | Maximum single purchase | 10,000,000 credits ($10,000) | | Precision | Integer (no fractional credits) | | Storage | float64 in IAM user balance field (USD-denominated) |
Credits are stored as a USD-denominated float in IAM (1,000 credits = $1.00 balance). The "credit" is a user-facing abstraction; the IAM balance field stores the dollar equivalent. This means 20,000 credits = $20.00 balance.
Bulk purchases receive volume discounts:
| Purchase Amount | Credits | Bonus | Effective Rate | |----------------|---------|-------|----------------| | $5 | 5,000 | 0% | $0.001/credit | | $20 | 21,000 | 5% | $0.000952/credit | | $50 | 55,000 | 10% | $0.000909/credit | | $100 | 115,000 | 15% | $0.000870/credit | | $500 | 600,000 | 20% | $0.000833/credit | | $1,000+ | Custom | 25%+ | Negotiated |
Credit costs are derived from provider pricing plus a margin. The LLM Gateway publishes a rate card:
| Operation | Model Tier | Credits | Approx. USD | |-----------|-----------|---------|-------------| | Chat completion (1K input tokens) | Economy (Mixtral, Llama) | 1 | $0.001 | | Chat completion (1K input tokens) | Standard (GPT-4-Turbo, Claude Sonnet) | 10 | $0.01 | | Chat completion (1K input tokens) | Premium (GPT-4, Claude Opus) | 30 | $0.03 | | Chat completion (1K output tokens) | Economy | 2 | $0.002 | | Chat completion (1K output tokens) | Standard | 15 | $0.015 | | Chat completion (1K output tokens) | Premium | 60 | $0.06 | | Image generation (1024x1024) | DALL-E 3 | 40 | $0.04 | | Embedding (1K tokens) | text-embedding-3-small | 0.1 | $0.0001 | | Audio transcription (1 minute) | Whisper | 6 | $0.006 | | Computer use (1 action) | Operative | 5 | $0.005 |
These rates are stored in the LLM Gateway configuration and updated when provider pricing changes. Commerce does not need to know the rates; it only processes the debit transactions that Gateway and Cloud submit to IAM.
tiers:
free:
name: "Free"
price_monthly: 0
credits_monthly: 1000
psp_price_id: null
overage: blocked
features:
- "1,000 credits/month (~100 GPT-4 messages)"
- "Community support"
- "3 requests/minute rate limit"
- "Standard models only"
pro:
name: "Pro"
price_monthly: 20
credits_monthly: 50000
psp_price_id: "price_pro_monthly"
overage: pay_as_you_go
features:
- "50,000 credits/month (~5,000 GPT-4 messages)"
- "Priority support"
- "60 requests/minute rate limit"
- "All models including Premium tier"
- "MCP tool access"
- "Usage analytics dashboard"
team:
name: "Team"
price_monthly: 100
credits_monthly: 150000
psp_price_id: "price_team_monthly"
overage: pay_as_you_go
features:
- "150,000 credits/month"
- "5 team members included ($15/additional)"
- "Shared org billing"
- "120 requests/minute rate limit"
- "Admin dashboard"
- "SSO via IAM (HIP-26)"
enterprise:
name: "Enterprise"
price_monthly: custom
credits_monthly: custom
psp_price_id: "price_enterprise_custom"
overage: invoice
features:
- "Custom credit allocation"
- "Unlimited team members"
- "Dedicated support and SLA"
- "Custom rate limits"
- "Volume discounts (25%+)"
- "Invoice billing (NET 30)"
- "SOC 2 compliance reports"
Free-tier credits reset monthly and do not accumulate. Paid-tier included credits roll over for 90 days. Purchased credits (one-time top-ups) never expire.
invoice.paid -> Commerce credits IAM with included credits.invoice.paid -> Commerce adds monthly credits. Unused credits from previous months roll over for 90 days.past_due. After 14 days past_due, the subscription is canceled and the user is downgraded to Free.1. Client: POST /v1/billing/checkout { amount: 2000, currency: "usd", credits: 21000 }
2. Commerce creates a PSP checkout session with metadata (user_id, org_id, credits, idempotency_key)
3. Commerce returns checkout URL -> client redirects user to the native hosted checkout
4. User completes payment on the native hosted checkout page (Hanzo Vault CDE)
5. PSP fires webhook: checkout.session.completed
6. Commerce verifies webhook signature (HMAC-SHA256)
7. Commerce checks idempotency key in KV (prevent double-processing)
8. Commerce calls IAM: POST /api/add-balance { owner: "hanzo", user: "z", amount: 21.0 }
9. Commerce records transaction: POST /api/add-transaction
{ category: "Recharge", user: "z", amount: 21.0, name: "txn_psp_cs_..." }
10. User's IAM balance updated. Credits available immediately.
Commerce also accepts $AI token (HIP-1) payments on Hanzo Network (chain ID 36963). The user sends tokens to a per-user deposit address. Commerce's on-chain listener detects the Transfer event (1 block confirmation for <$1K, 6 for >=$1K), converts $AI to USD at the 10-minute TWAP oracle rate via HMM (HIP-8), and credits the user's IAM balance through the same add-balance/add-transaction flow as fiat.
| Method | Endpoint | Description | Auth | |--------|----------|-------------|------| | GET | /v1/billing/balance | Current credit balance | Bearer token | | GET | /v1/billing/transactions | Transaction history with pagination | Bearer token | | POST | /v1/billing/checkout | Create PSP checkout session | Bearer token | | POST | /v1/billing/checkout/crypto | Create crypto payment intent | Bearer token | | POST | /v1/billing/subscribe | Create or change subscription | Bearer token | | DELETE | /v1/billing/subscribe | Cancel subscription | Bearer token | | GET | /v1/billing/subscription | Current subscription details | Bearer token | | GET | /v1/billing/invoices | List invoices | Bearer token | | GET | /v1/billing/invoices/:id | Download invoice PDF | Bearer token | | GET | /v1/billing/usage | Usage breakdown by period | Bearer token | | POST | /v1/billing/portal | Create PSP customer portal session | Bearer token | | POST | /webhooks/psp | PSP webhook receiver | PSP signature |
GET /v1/billing/balance returns the user's current credit state:
{
"balance": 15420, "balance_usd": 15.42, "tier": "pro",
"credits_included": 50000, "credits_used": 34580, "credits_remaining": 15420,
"period_start": "2026-02-01T00:00:00Z", "period_end": "2026-03-01T00:00:00Z",
"auto_recharge": { "enabled": true, "threshold": 1000, "amount": 20000 }
}
GET /v1/billing/transactions returns paginated transaction history. Each transaction includes id, timestamp, category (Purchase/Recharge), credits (negative for debits), balance_after, and metadata with model/token/provider details.
GET /v1/billing/usage?period=2026-02 returns usage aggregated by model (gpt-4-turbo, claude-3-sonnet, etc.), by service (llm-gateway, cloud, mcp), and optionally by day when granularity=daily is specified.
Commerce receives webhooks from the native PSP for all payment-related events. The webhook handler follows a strict pipeline:
async def handle_psp_webhook(request):
# 1. Verify signature (CRITICAL - prevents forgery)
payload = request.body
signature = request.headers["X-Webhook-Signature"]
try:
event = psp.verify_event(
payload, signature, psp_webhook_secret
)
except psp.SignatureError:
return Response(status=400, body="Invalid signature")
# 2. Check idempotency (prevent double-processing)
event_id = event["id"]
if await redis.exists(f"webhook:processed:{event_id}"):
return Response(status=200, body="Already processed")
# 3. Route by event type
handlers = {
"checkout.session.completed": handle_checkout_completed,
"invoice.paid": handle_invoice_paid,
"invoice.payment_failed": handle_payment_failed,
"customer.subscription.updated": handle_subscription_updated,
"customer.subscription.deleted": handle_subscription_deleted,
"charge.dispute.created": handle_dispute_created,
"charge.refunded": handle_refund,
}
handler = handlers.get(event["type"])
if handler:
await handler(event)
# 4. Mark as processed (72h TTL matching the PSP retry window)
await redis.set(f"webhook:processed:{event_id}", "1", ex=259200)
return Response(status=200)
| Event | Action | |-------|--------| | checkout.session.completed | Add credits to user's IAM balance | | invoice.paid | Add subscription credits; record payment | | invoice.payment_failed | Send warning email; flag account | | customer.subscription.updated | Update tier in IAM user properties | | customer.subscription.deleted | Downgrade to Free tier | | charge.dispute.created | Freeze account; debit disputed amount; create support ticket | | charge.dispute.closed | Unfreeze if won; maintain debit if lost | | charge.refunded | Debit refunded credits from IAM balance |
The LLM Gateway and Cloud services do not interact with Commerce directly for debits. They submit usage to IAM via the transaction API:
LLM Gateway receives request
|
+-- 1. Validate JWT (IAM)
+-- 2. Check balance from JWT claims or /api/get-account
+-- 3. If insufficient balance: return 402 Payment Required
+-- 4. Execute LLM request (provider API)
+-- 5. Calculate cost:
| input_tokens * input_rate + output_tokens * output_rate
| -> convert to USD -> round to credits
+-- 6. Submit debit transaction to IAM:
| POST /api/add-transaction
| {
| "category": "Purchase",
| "user": "<user>",
| "amount": -0.012, // negative = debit
| "detail": "gpt-4-turbo: 847 in + 400 out tokens"
| }
+-- 7. Return response to user
The Gateway batches transaction submissions. Rather than calling IAM for every request, it accumulates usage per user over a 10-second window and submits a single aggregated transaction. This reduces IAM load by ~90% during high-throughput periods.
metering:
batch_interval: 10s
batch_max_size: 100
minimum_charge: 0.001 # 1 credit minimum debit
balance_check: jwt_claims # or "api_call"
insufficient_balance_code: 402
retry_on_iam_failure: true
retry_max_attempts: 3
retry_backoff: exponential
Each organization in IAM has independent billing. A user who belongs to multiple organizations (e.g., z@hanzo.ai is a member of both hanzo and zoo) has a separate balance in each organization context.
Organization: hanzo
User: z
Balance: $150.00
Tier: Enterprise
Transactions: [org-scoped]
Organization: zoo
User: z
Balance: $25.00
Tier: Pro
Transactions: [org-scoped]
When the LLM Gateway processes a request, the organization context is determined by the OAuth application that issued the token. A token from app-cloud (organization: hanzo) bills against the hanzo org balance. A token from app-zoo bills against the zoo org balance.
Organizations can choose between two billing modes:
/v1/billing/refund.psp.refunds.create) for the original payment intent.charge.refunded, Commerce debits the refunded credits from IAM via add-transaction with negative amount.charge.dispute.created.suspended=true in IAM), debits the disputed amount, creates a support ticket, and notifies the admin team.charge.dispute.closed: if won, Commerce unfreezes the account and restores the debited amount; if lost, the account remains suspended until the balance is positive.Commerce generates monthly invoices for all users with non-zero activity. Each invoice includes:
Invoices are stored as PDFs in MinIO (HIP-32) and emailed to the user. Enterprise customers on Net-30 receive invoices with payment instructions instead of automatic billing.
Users can configure automatic credit purchases when their balance drops below a threshold:
POST /v1/billing/auto-recharge
Authorization: Bearer <access_token>
{
"enabled": true,
"threshold": 1000,
"amount": 20000,
"max_monthly": 5,
"payment_method": "pm_..."
}
When the metering pipeline detects a balance crossing below the threshold, it enqueues an auto-recharge job. The job creates a PSP payment intent using the stored (tokenized) payment method, processes the charge, and credits the balance without user interaction. Auto-recharge is rate-limited (max 5/month by default) to prevent runaway charges from buggy clients or compromised API keys.
invoice.paidCommerce achieves PCI compliance by never handling cardholder data. Cardholder data is confined to the Hanzo Vault CDE — the only PCI-DSS-scoped component — which hosts the payment form (native hosted checkout) and issues opaque tokens. Commerce and every app server operate on tokens only; they never see, store, process, or transmit card numbers, CVVs, or expiration dates. Commerce qualifies as a SAQ A surface; the Vault carries the full SAQ D / RoC scope, isolated from the rest of the ecosystem.
Every native-PSP webhook is verified using HMAC-SHA256:
# The PSP signs webhooks with the webhook signing secret (held in KMS).
# The signature includes a timestamp to prevent replay attacks.
signature = request.headers["X-Webhook-Signature"]
# Format: t=<timestamp>,v1=<signature>
expected = hmac_sha256(
key=psp_webhook_secret, # from KMS, never plaintext
message=f"{timestamp}.{payload}"
)
# Reject if:
# 1. Signature does not match (forged webhook)
# 2. Timestamp is > 300 seconds old (replay attack)
# 3. Event ID already processed (duplicate delivery)
Every payment operation uses idempotency keys to prevent double-charging:
idempotency_key in session metadata ensures retried webhooks do not create duplicate credits.name field (txn_psp_{event_id}) acts as a unique constraint. IAM rejects duplicate transaction names.These three layers provide defense-in-depth against double-processing.
| Endpoint | Limit | Window | |----------|-------|--------| | /v1/billing/checkout | 10 | per minute per user | | /v1/billing/subscribe | 5 | per minute per user | | /v1/billing/balance | 60 | per minute per user | | /v1/billing/transactions | 30 | per minute per user | | /v1/billing/usage | 10 | per minute per user | | /webhooks/psp | 1000 | per minute (global) |
Rate limiting is enforced via KV sliding window counters. Exceeding the limit returns 429 Too Many Requests with a Retry-After header.
Every financial event produces an immutable audit record:
{
"event_id": "evt_audit_001",
"timestamp": "2026-02-23T10:30:15.123Z",
"action": "credit_purchase",
"actor": {
"user_id": "hanzo/z",
"ip": "203.0.113.42",
"user_agent": "Mozilla/5.0..."
},
"details": {
"psp_event_id": "evt_...",
"psp_session_id": "cs_...",
"amount_usd": 20.00,
"credits_added": 21000,
"balance_before": 5000,
"balance_after": 26000,
"idempotency_key": "ik_abc123"
},
"integrity": "sha256:ab3f..."
}
Audit records are append-only (never updated or deleted), integrity-protected with SHA-256 hash chains, retained for 7 years (financial regulatory minimum), and exportable for SOC 2 and compliance audits.
When a user's balance reaches zero during an API request:
402 Payment Required with a link to the billing page.github.com/lux-pay (white-labeled lux-pay on Lux)Copyright and related rights waived via CC0.