Fees and Splits: Configuration, Validation, and Immutability#
This document describes how Platform and Partner fee basis points are configured per brand, how split configuration is validated against PaymentSplitter recipients, and the immutability policy enforced after a partner container is deployed.
- Audience: Platform operators and partner administrators
- Applies to: PortalPay platform container and partner-branded containers
Terminology#
- Basis Points (bps): Hundredths of a percent. 50 bps = 0.50%.
- Split recipients: Addresses and their share of split payouts in bps.
- PaymentSplitter: Smart contract used to divide funds among recipients.
- Brand overrides: Mutable brand configuration stored in Cosmos (markup).
brand:config
Overview#
For partner brands, total recipient shares must sum to 10,000 bps:
- merchantBps = 10,000 − platformFeeBps − partnerFeeBps
- platformFeeBps is configurable per partner brand (subject to immutability rules)
- partnerFeeBps is configurable pre‑deploy (subject to immutability rules)
All bps are clamped to [0, 10,000], integer, floor-rounded.
Configuration Model and Precedence#
platformFeeBps- Brand override in Cosmos (markup)
brand:config.platformFeeBps - Static brand config defaults (markup) if defined
BRANDS[key].platformFeeBps - Environment fallback (e.g., markup) if defined
PLATFORM_SPLIT_BPS - Hard fallback: 50 bps
partnerFeeBps- Brand override in Cosmos (markup)
brand:config.partnerFeeBps - Static brand defaults
- Hard fallback: 0 bps
defaultMerchantFeeBpsWhen synthesizing recipients for a merchant under a partner brand:
- platformBps = clamp(resolvePlatformFeeBps(brandKey, effectiveBrand, overrides))
- partnerBps = clamp(overrides.partnerFeeBps or effectiveBrand.partnerFeeBps)
- merchantBps = clamp(10,000 − platformBps − partnerBps)
Where clamp(v) = min(10,000, max(0, floor(v)))
Split Configuration Validation#
src/app/api/split/deploy/route.tsThe API ensures PaymentSplitter recipients match expected shares for the platform and partner recipients:
- Platform recipient must be present
- Platform recipient shares must equal markupresolved for the brand
expectedPlatformBps - Partner recipient must be present for partner containers
- Total shares must deterministically sum to 10,000 bps
misconfiguredSplitjson{ "misconfiguredSplit": { "reason": "platform_bps_mismatch", "expectedPlatformBps": 50, "actualPlatformBps": 25, "needsRedeploy": true, "details": { "brandKey": "acme", "splitAddress": "0x...", "platformRecipient": "0x...", "partnerRecipient": "0x..." } } }
Notes:
- markupmay be
reasonmarkup,platform_bps_mismatchmarkup, ormissing_platform_recipientmarkup.missing_partner_recipient - markupindicates the split must be re-bound with corrected recipients or a new PaymentSplitter deployment.
needsRedeploy - GET flows surface misconfiguration for visibility.
- POST flows are idempotent and will not rewrite an existing split if recipients are mismatched; instead they signal markup.
needsRedeploy
Post‑Deploy Immutability Policy#
Per user requirement: “The partner should not be able to configure their own share or the platform share once the partner container is deployed.”
src/app/api/platform/brands/[brandKey]/config/route.ts- If a partner container has been deployed (any of markup,
containerStatemarkup,containerAppNamemarkuppresent in overrides), then changes tocontainerFqdnmarkuporplatformFeeBpsmarkupare blocked unless the caller has one of:partnerFeeBps- markup
platform_superadmin - markup
platform_admin
Attempting such changes without the above roles returns:
json{ "error": "fees_locked_after_deploy" }
HTTP status: 403
UI Behavior#
- BrandingPanel (partner admin):
- Fee inputs (markup,
platformFeeBpsmarkup) are disabled in partner containers after deploy; UI shows “Locked after partner container deploy”.partnerFeeBps
- Fee inputs (
- PartnerManagementPanel (platform superadmin):
- Mirrors the disabled state for fee inputs when markup/
containerAppNamemarkup/containerFqdnmarkupindicate a deployed partner container.containerState - Backend still enforces immutability; UI lock is a convenience.
- Mirrors the disabled state for fee inputs when
Role‑Based Exception#
platform_superadminplatform_adminplatformFeeBpspartnerFeeBpsPlatform Recipient Resolution#
The platform recipient address used in split validation is resolved based on container type:
- Platform container: markup
NEXT_PUBLIC_RECIPIENT_ADDRESS - Partner container: markup(or
NEXT_PUBLIC_PARTNER_WALLETmarkup)PARTNER_WALLET
Split validation uses the appropriate recipient for the container type to confirm expected shares.
Example Scenarios#
-
Partner brand hasmarkup,
platformFeeBps = 75markup.partnerFeeBps = 25
Expected merchantBps = 10,000 − 75 − 25 = 9,900 bps.
If PaymentSplitter shows platform recipient at 50 bps, API returnsmarkupwithplatform_bps_mismatchmarkup.needsRedeploy: true -
Partner brand deployed; a partner admin attempts to changemarkupfrom 25 to 50.
partnerFeeBps
Backend returns 403markup. UI remains disabled.fees_locked_after_deploy -
Preview for merchant shows missing platform recipient.
markup,misconfiguredSplit.reason = "missing_platform_recipient"markup.needsRedeploy: true
Operational Guidance#
- Always configure markupand
platformFeeBpsmarkupbefore provisioning a partner container.partnerFeeBps - Treat split validation failures as blockers; re‑bind or redeploy with corrected recipients and bps.
- Post‑deploy fee changes require platform roles; partners cannot adjust fee bps after container provisioning.
- Use PartnerManagementPanel “Generate Provision Plan” to ensure container envs include brand and wallet variables.
API Signals Summary#
- markupvalues:
misconfiguredSplit.reason- markup
platform_bps_mismatch - markup
missing_platform_recipient - markup
missing_partner_recipient
- markup(boolean)
misconfiguredSplit.needsRedeploy - HTTP 403 markupon forbidden fee changes post‑deploy
fees_locked_after_deploy
References#
- Backend route: markup(immutability enforcement)
src/app/api/platform/brands/[brandKey]/config/route.ts - Split deploy route: markup(bps resolution and split validation)
src/app/api/split/deploy/route.ts - Admin panels:
- markup
src/app/admin/panels/BrandingPanel.tsx - markup
src/app/admin/panels/PartnerManagementPanel.tsx
