White-Label Tenants
This guide covers UI tenants — branded investor-facing hostnames on the single investhubio deployment. Operators (issuers, Dagobert, liquidation cases) get their own subdomain, logo, and deal filter while sharing the same app, KYC, and compliance stack under the ECSP umbrella.
Terminology: A UI tenant is not the legacy KYC SaaS tenant (separate GraphQL stack per issuer, e.g. old Envion liquidation). See
docs/planning/MULTI_TENANT_ARCHITECTURE.md.
x402 Protocol: The tenant provisioning API uses the x402 protocol for payment. On first call, the API returns HTTP 402 with USDC payment instructions. The client signs an ERC-3009
transferWithAuthorization, then retries with the signed payload in theX-PAYMENTheader. Payment is verified and settled on-chain via the Coinbase facilitator before the tenant is created.
Overview
A UI tenant is a branded hostname (e.g. envion.investhub.io, dagobert.investhub.org) served from one React + Supabase deployment. TenantContext resolves config at runtime from the request hostname. Each issuer UI can show a different subset of deals via allowedTokenIds; KYC and invest flows are platform-wide, not per-repo.
Each tenant can configure:
appName
Display name shown in browser title and UI
logoUrl
Primary logo (light mode)
logoDarkUrl
Logo for dark mode
faviconUrl
Browser favicon
primaryColor
HSL primary color (e.g. 210 100% 50%)
accentColor
HSL accent color
allowedTokenIds
Array of token IDs visible to this tenant's users
hiddenRoutes
Array of URL paths to hide from navigation
privyAppId
Optional tenant-specific Privy application ID
Pricing
Setup
5,000 USDC
One-time, paid via x402 on Base
Monthly renewal
39 USDC
Push model — owner pays each month via x402 on Base
If a tenant's subscription expires and is not renewed, the tenant is automatically suspended (STOPPED) by a cron job. Suspended tenants lose their custom branding and fall back to the default InvestHub experience.
Setup flow
Renewal flow
API Reference
All endpoints use Privy JWT authentication. Include Authorization: Bearer <privy_access_token> in every request.
POST /functions/v1/create-tenant-x402
Create a new white-label tenant. Payment-gated at 5,000 USDC on Base.
Request body:
subdomainType
"investhubSubdomain" | "ownSubdomain"
Yes
Use investhubSubdomain for *.investhub.io (DNS auto-configured) or ownSubdomain for a custom domain (manual DNS)
customDomain
string
Yes
Subdomain name (e.g. buyersclub) or full domain (e.g. app.buyersclub.com)
appName
string
No
Display name
logoUrl
string
No
Logo URL (light mode)
logoDarkUrl
string
No
Logo URL (dark mode)
primaryColor
string
No
HSL color string
accentColor
string
No
HSL color string
allowedTokenIds
number[]
No
Token IDs to show (null = show all)
hiddenRoutes
string[]
No
Routes to hide from navigation
Responses:
402 (no
X-PAYMENTheader): Returns payment instructions.
200 (with valid
X-PAYMENTheader): Tenant created.
POST /functions/v1/renew-tenant-x402
Renew the subscription for an existing tenant. Payment-gated at 39 USDC on Base.
Request body: None required (tenant is resolved from the authenticated user's profile).
Responses:
402 (no
X-PAYMENTheader): Returns payment instructions for 39 USDC.200 (with valid
X-PAYMENTheader): Subscription extended by 30 days.
GET /functions/v1/get-tenant-config
Public endpoint. Returns branding configuration for the tenant matching the Origin header's hostname. No authentication required.
Response:
POST /functions/v1/check-tenant-subscriptions
Internal/cron endpoint. Suspends all tenants whose subscription_paid_until has passed. No authentication required (intended for cron/scheduler invocation).
Response:
Agent integration example
An AI agent or script can provision a tenant programmatically using the x402 fetch wrapper:
The @x402/fetch wrapper automatically:
Detects the 402 response
Signs an ERC-3009
transferWithAuthorizationfor the required USDC amountRetries the request with the
X-PAYMENTheader
Manual testing with curl
You can also test the x402 flow manually in two steps:
Step 1 — Get payment instructions:
This returns HTTP 402 with the payment requirements JSON.
Step 2 — Pay and create:
After signing the ERC-3009 authorization off-chain, retry with the payment payload:
Database schema
tenants table (relevant columns)
subscription_paid_until
timestamptz
When the paid period expires
app_name
text
Display name
logo_url
text
Logo (light mode)
logo_dark_url
text
Logo (dark mode)
favicon_url
text
Favicon URL
primary_color
text
HSL primary color
accent_color
text
HSL accent color
allowed_token_ids
integer[]
Token IDs visible to tenant users
hidden_routes
text[]
Routes hidden from navigation
privy_app_id
text
Tenant-specific Privy app ID
tenant_payments table
id
uuid
Primary key
tenant_id
uuid
FK to tenants
user_id
uuid
FK to profiles
payment_type
text
setup or recurring
amount_usdc
numeric(18,6)
Payment amount
tx_hash
text
On-chain transaction hash (unique)
chain
text
Blockchain (default: base)
network_id
text
EIP-155 network ID (default: eip155:8453)
status
text
pending, confirmed, or failed
period_start
timestamptz
Subscription period start
period_end
timestamptz
Subscription period end
Last updated