R0 Soft Financial is the recent, legally-most-proven financial model. It is fully implemented on branch codex/r0-soft-financial-dev-merge of the state-server repo (tip commit 75760ea, 2026-05-04). CI on that branch is green (pipeline 19433, 2026-05-12). It is not yet merged to dev (verified via git merge-base --is-ancestor) and no merge request is open (GitLab API returns empty). Production currently runs the legacy primitive — the simple credit ledger on dev (HEAD 41fe6f2, 2026-05-05).
This page compares every financial-handling approach that exists or has existed in the project, explains why R0 is the recent direction, and details its production-readiness gating items.
| Approach | Status | VAT | Cool-off | Trader verif. | Stripe webhook | Idempotency | Typed ledger | Audit events | Refund codes |
|---|---|---|---|---|---|---|---|---|---|
| Karma (legacy concept) | No code; abandoned strategic note | – | – | – | – | – | – | – | – |
Simple Credit Ledger (dev) |
Production today | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| R0 Soft Financial (branch) | Implemented, awaiting merge + manual deploy | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| DomiDo PayDo | Archival (2018–2019), separate product line | – | – | – | – | – | – | – | – |
| Master-task-spec snapshot | Frozen design + release notes; FINANCIAL_BACKEND.md is byte-identical to the R0 branch copy |
mirrors R0 | mirrors R0 | mirrors R0 | mirrors R0 | mirrors R0 | mirrors R0 | mirrors R0 | mirrors R0 |
avvy-app wallet.jsx |
Mock UI; the shape (offers, auto-topup thresholds, verification pills) targets R0 endpoints, not the legacy ledger | UI scaffold | UI scaffold | UI scaffold | n/a | n/a | n/a | n/a | n/a |
The primitive on dev is a per-user signed-int balance with three transaction kinds (deposit, withdrawal, transfer) and a non-negative invariant. It cannot tell a deposit from a sale, has no notion of VAT, no refund window, no Stripe coupling, and nothing resembling an audit trail. It is suitable for "give the user some non-monetary credits and let them spend them"; it is not a marketplace payment system. Operating a paid marketplace on top of it would breach UK Consumer Contracts Regulations 2013 (right of withdrawal), HMRC VAT-registration rules (VAT-inclusive pricing + tax point on credit purchase), and PSD2 / Stripe TOS (signed webhook handling).
R0 closes every one of those gaps with code, not just docs. Each compliance concern below is backed by a citable file path on the R0 branch.
All paths below are on the R0 branch origin/codex/r0-soft-financial-dev-merge. Read with git show <ref>:<path>. The current dev branch does not contain these files.
(R0 branch) internal/domain/entity/credits.go — defines:
CreditAccountOwnerKind: user, company, platform.CreditAccountKind: customer_spendable, supplier_earnings, platform_fee, pending_adjustment.CreditTransactionKind: deposit, withdrawal, transfer, purchase_debit, platform_fee, supplier_credit, refund, dispute_reversal, ppc_spend, ppc_publisher_credit.CreditTransaction carries VATAmount, VATRate, VATJurisdiction, StripeTaxCalculationID, FundingTrancheID, AccountKind on every line — none of which exist on the dev version of this file.(R0 branch) internal/domain/billing/types.go — OwnerKind (user / company), PaymentKind (topup / setup / auto_topup), PaymentStatus (created → pending → succeeded / failed / expired / refunded / disputed), RefundStatus (none → requested → refunded), and FundingTranche with tranche_id, credits_total, credits_remaining, cooling_off_expires_at, vat_amount, vat_rate, vat_jurisdiction, stripe_tax_calculation_id.
(R0 branch) internal/domain/entity/{purchase.go, purchase_quote.go} — Purchase, Entitlement, plus PurchaseRejectReason (quote_expired, quote_stale, insufficient_balance, seller_trader_not_verified, allocation_invalid, already_purchased) and EntitlementCheckDenyReason.
(R0 branch) internal/domain/entity/company_account.go — IsVerifiedTraderProfile() gates supplier credits on verification_status == "verified", non-empty legal_name, valid trader_type (company / llc / llp / sole_trader / self_employed / other_business), vat_registration_status ∈ {registered, not_registered}, trader_self_certification_accepted == true, plus vat_registration_id when registered.
(R0 branch) internal/domain/idempotency/{types.go, repository.go} — Record with Op, RequestHash, CreatedAtUnix, ExpiresAtUnix, Result; ResultKind covers payment, wallet_refund, purchase_quote, purchase.
(R0 branch) internal/infrastructure/persistence/fdb/:
credit_repository.go — typed-ledger keyspace /credits/{account_kind}/{owner_kind}/{owner_id}/{tx_id}; atomic multi-line posts; recomputed BalanceAfter; duplicate tx_id rejection.billing_repository.go — /billing/accounts/{owner_kind}/{owner_id}, /billing/payments/{id}, and /billing/webhooks/{provider}/{event_id} for webhook deduplication.idempotency_repository.go — primary /idempotency/records/{user_id}/{key} + secondary /idempotency/expiry/{expires_at}/{user_id}/{key} for TTL sweeps.purchase_quote_repository.go — /commerce/quotes/{quote_id}, ~30-minute expiry, consumed on execute.purchase_repository.go — immutable /commerce/purchases/{purchase_id}.entitlement_repository.go — /commerce/entitlements/{entitlement_id}, secondary indexes by (buyer_id, object_id) and (buyer_id, purchase_id).commerce_audit_repository.go — /commerce/audit/{purchase_id}/{event_type} with by-id and by-purchase indexes; duplicates fail with ErrAlreadyExists. Event types include eco.purchase, eco.platform_fee, eco.contributor_credit.purchase_execution_fault_repository.go — fault-injection harness for rollback proofs.transaction.go — txManager that scopes the atomic-multi-line writes.(R0 branch) internal/services/:
credit_ledger_service.go — GetAccountBalance, ListAccountTransactions, AppendTransactions (atomic multi-line). LowBalance + Debit observers feed auto-topup.billing_service.go — Checkout / SetupIntent session creation, payment-method lifecycle, auto-topup logic, webhook routing. Scoped methods exist as *ForScope(userID, scope, ...) covering user-owned and company-owned billing surfaces. HandleWebhook(ctx, payload, signature) parses Stripe events and idempotently routes them.billing_tranches.go, billing_audit.go, billing_account_helpers.go, billing_scope.go, billing_scoped_api.go — tranche lifecycle, audit recording, scope helpers.wallet_service.go — user-scoped façade over billing + ledger: GetBalanceForScope, ListTransactionsForScope (customer_spendable only), CreateFundingSessionForScope, RequestRefundForScope (enforces cooling-off window + spend status, returns the five-code refusal enum).purchase_quote_service.go — guest handoff (unauthenticated, rate-limited) + authenticated quote. Validates visibility, purchasability, verified-trader state, quote expiry. Returns raw server fields only — no VAT / Stripe / cart fields leak.purchase_execution_service.go — ExecutePurchase is the atomic write: one FDB transaction that posts the buyer debit on customer_spendable, the platform-fee allocation, the supplier credit(s) on supplier_earnings, the purchase record, the entitlement(s), the commerce audit events, and the idempotency replay record. Any failure rolls everything back.entitlement_service.go — ListEntitlements, GetEntitlement, CheckEntitlement (returns {allowed, reason?} on success rather than throwing for ordinary no-access cases).supplier_earnings_service.go — read-only, company-scoped: GetBalance, ListTransactions (supplier_earnings account only), GetSummary (settled = balance, pending = 0, refunded = 0, disputed = 0 in R0). Authorisation requires IsVerifiedTraderProfile() + membership.(R0 branch) internal/interfaces/http/handlers/{wallet, billing, commerce, earnings, entitlements}/handler.go — one package per surface. (R0 branch) internal/interfaces/http/middleware/idempotency.go enforces Idempotency-Key on RequiredPathPrefixes /api/v1/billing, /api/v1/wallet, /api/v1/commerce, with 24-hour TTL, SHA-256 request fingerprinting, 422 on body mismatch, and minimal-state replay (Result.ID, Result.IDs, Result.Meta).
(R0 branch) internal/infrastructure/payments/stripe/provider.go — CreatePaymentCheckoutSession sets AutomaticTax.Enabled: true and BillingAddressCollection: required. ParseWebhook calls webhook.ConstructEventWithOptions(..., IgnoreAPIVersionMismatch: true) so a Stripe API-version drift does not break receipt while signature verification stays enforced. Webhook subscription set: checkout.session.completed, checkout.session.expired, payment_intent.succeeded, payment_intent.payment_failed, setup_intent.succeeded, charge.refunded, charge.dispute.created.
(R0 branch) internal/infrastructure/payments/fake/provider.go — test seam mirroring the same interface for integration / E2E.
(R0 branch) sdk/js/src/services/{wallet, billing, commerce, earnings}-service.js — public method surface matches the endpoint catalogue. sdk/js/scripts/run-stripe-wallet-certification.mjs is the Stripe-live certification harness.
Each compliance concern, mapped to where it's enforced.
VAT collection at credit-purchase time. Stripe Tax with automatic_tax.enabled=true and required billing-address collection — (R0 branch) internal/infrastructure/payments/stripe/provider.go::CreatePaymentCheckoutSession. Persisted as vat_amount / vat_rate / vat_jurisdiction / stripe_tax_calculation_id on every deposit transaction and on the FundingTranche record.
Tax-cleared credits invariant. Wallet-funded purchases (typed-ledger transfers between accounts) do not call Stripe Tax and do not emit VAT fields. Enforced by the request-shaping rejection of VAT-bearing fields in (R0 branch) internal/interfaces/http/handlers/commerce/handler.go.
Cooling-off window. Per-tranche window stored in FundingTranche.cooling_off_expires_at. Refund eligibility checked in (R0 branch) internal/services/wallet_service.go::RequestRefundForScope. Refusal enum: outside_cooling_off_window, tranche_already_spent, refund_already_requested, tranche_not_found, payment_not_refundable.
Verified trader gating. (R0 branch) internal/domain/entity/company_account.go::IsVerifiedTraderProfile() is called in (R0 branch) internal/services/purchase_execution_service.go::executePurchaseTx (before crediting supplier) and (R0 branch) internal/services/supplier_earnings_service.go::authorizeCompanyRead (before any read). Failure: PurchaseRejectReasonSellerTraderNotVerified.
Stripe webhook signature verification. (R0 branch) internal/infrastructure/payments/stripe/provider.go::ParseWebhook uses the Stripe SDK's webhook.ConstructEventWithOptions against BILLING_STRIPE_WEBHOOK_SECRET; empty secret returns an error rather than silently accepting. Webhook events are deduplicated in (R0 branch) internal/infrastructure/persistence/fdb/billing_repository.go::RecordWebhookEvent.
Idempotency. (R0 branch) internal/interfaces/http/middleware/idempotency.go requires Idempotency-Key on all mutating /api/v1/{wallet, billing, commerce} requests. Conflict body returns 422; identical retry replays the original response from the persisted minimal state.
Atomic ledger writes. (R0 branch) internal/services/purchase_execution_service.go::ExecutePurchase writes — inside one FDB transaction — the buyer debit + platform fee + supplier credit(s) + purchase record + entitlement(s) + commerce audit events + idempotency replay record. Rollback is all-or-nothing.
Commerce audit trail. (R0 branch) internal/infrastructure/persistence/fdb/commerce_audit_repository.go::CreateMany writes the audit records inside the same transaction.
User-scoped (auth required unless noted). All Idempotency-Key rows return 422 on body conflict with an in-flight key, replay on identical retry.
| Method | Path | Handler | Idempotency-Key | Summary |
|---|---|---|---|---|
| GET | /wallet/balance |
wallet | – | Spendable balance + active tranches. |
| GET | /wallet/transactions |
wallet | – | customer_spendable history (paginated). |
| GET | /wallet/offers |
wallet | – | Public offers; no Stripe fields. |
| POST | /wallet/funding-sessions |
wallet | required | Create Stripe Checkout for top-up. |
| POST | /wallet/refunds |
wallet | required | Request tranche refund within cooling-off window. |
| GET | /billing/offers |
billing | – | Same offers as wallet. |
| POST | /billing/checkout/session |
billing | required | Alt Checkout entry point. |
| POST | /billing/setup/session |
billing | required | SetupIntent to save a card. |
| GET / DELETE | /billing/payment-method |
billing | – | Default card summary / detach. |
| GET / PUT | /billing/auto-topup |
billing | required (PUT) | Threshold + amount + monthly limit. |
| GET | /billing/payments |
billing | – | Payment history. |
| POST | /api/v1/webhooks/stripe |
wallet | – | Signed Stripe webhook receiver. |
| POST | /commerce/purchase-handoffs |
commerce | – | Guest intent capture; unauthenticated, rate-limited. |
| POST | /commerce/purchase-quotes |
commerce | required | Authenticated read-only quote. |
| POST | /commerce/purchases |
commerce | required | Atomic wallet-funded execution. |
| GET | /commerce/purchases/{purchase_id} |
commerce | – | Purchase record (buyer or seller). |
| GET | /entitlements |
entitlements | – | List buyer entitlements (paginated). |
| GET | /entitlements/{entitlement_id} |
entitlements | – | Fetch one. |
| POST | /entitlements/check |
entitlements | – | Gated-access probe; returns {allowed, reason?}. |
Company-scoped variants exist for every wallet / billing / earnings route under /api/v1/company/accounts/{company_id}/{wallet,billing,earnings}/…. Wallet + billing require company admin / member roles.
As of 2026-05-12, the state of the world (re-verified at execution time):
| Question | Answer | Evidence |
|---|---|---|
Merged to dev? |
No | git merge-base --is-ancestor origin/codex/r0-soft-financial-dev-merge origin/dev → false. |
| Merge request open? | No | GitLab GET /projects/304/merge_requests?source_branch=codex/r0-soft-financial-dev-merge&state=all → empty. |
| CI green on the branch? | Yes | Pipeline 19433 status success, updated 2026-05-12T15:04:13Z. |
| Deployed to production? | No | Production overlay pins gitlab.avvyland.com:5050/avvy/state-server:latest with imagePullPolicy: Always, but the Publish job in .gitlab-ci.yml is when: manual and has not been triggered. The :latest tag therefore predates the R0 branch. |
codex/r0-soft-financial-dev-merge into dev..gitlab-ci.yml (currently the test stage is commented; merging as-is would break the existing CI flow on dev).Publish job so a new :latest image is pushed to the GitLab registry.BILLING_ENABLED, BILLING_PROVIDER, BILLING_CURRENCY, BILLING_ALLOWED_RETURN_ORIGINS, BILLING_STRIPE_SECRET_KEY, BILLING_STRIPE_WEBHOOK_SECRET. New Ansible defaults are documented in (R0 branch) ansible/roles/server/defaults/main.yml (billing_enabled, billing_provider, billing_currency, billing_allowed_return_origins, billing_offers).stripe listen secret), set the public webhook endpoint to https://<api-domain>/api/v1/webhooks/stripe, subscribe to the seven webhook events listed in (R0 branch) docs/FINANCIAL_BACKEND.md §8.When all five are done, the production state-server will run R0; until then the production handler tree is dev's simple credit-ledger primitive only.
(dev) internal/domain/entity/credits.go defines a single per-user signed-int credit balance with three transaction kinds (deposit, withdrawal, transfer) and a non-negative invariant. (dev) internal/domain/entity/credit_repository.go::AppendTransaction is the atomic write boundary. (dev) internal/interfaces/http/handlers/user/handler.go::ListCreditTransactions serves GET /users/{id}/credits/transactions and GetCurrentUserState puts credits.balance on GET /user/state. No other financial handlers exist on dev. This is the legacy primitive that R0 supersedes (R0 retains backward compatibility — dev-style calls map to user/customer_spendable automatically in the new repository layer).
avvy-app/src/components/dashboard/content/wallet.jsx is already shaped against R0: the offers list, auto-topup controls (threshold / amount / monthly limit), and the three verification pills (email / age / payment method) correspond directly to R0 endpoints (/wallet/offers, /billing/auto-topup, /billing/payment-method plus user verification state). Today the component reads state.credits.balance from the live dev-side endpoint and falls back to mock transactions because R0's /users/{id}/credits/transactions extension is not yet live. When R0 lands, the mocks get replaced and the offer cards become server-driven.
State-server dev (HEAD 41fe6f2, 2026-05-05) — internal/domain/entity/{credits.go, credit_repository.go, credits_service.go}, internal/interfaces/http/handlers/user/{handler.go, routes.go}.
State-server R0 branch origin/codex/r0-soft-financial-dev-merge (tip 75760ea, 2026-05-04) — docs/FINANCIAL_BACKEND.md, RELEASE_NOTE_2026-04-21_STATE_SERVER_2.1.1_T01_TYPED_LEDGER_CORE.md, RELEASE_NOTE_2026-04-22_STATE_SERVER_2.1.2_T02_WALLET_AND_FUNDING_APIS.md, RELEASE_R0_SOFT_FINANCIAL_2026-05-04.md, internal/domain/entity/{credits.go, company_account.go, purchase.go, purchase_quote.go}, internal/domain/billing/{types.go, repository.go, provider.go}, internal/domain/idempotency/{types.go, repository.go}, internal/services/{wallet_service, billing_service, billing_tranches, billing_audit, credit_ledger_service, purchase_quote_service, purchase_execution_service, entitlement_service, supplier_earnings_service}.go, internal/infrastructure/payments/{stripe, fake}/provider.go, internal/infrastructure/persistence/fdb/{credit_repository, billing_repository, idempotency_repository, commerce_audit_repository, purchase_quote_repository, purchase_repository, entitlement_repository, purchase_execution_fault_repository, transaction.go}.go, internal/interfaces/http/handlers/{wallet, billing, commerce, earnings, entitlements}/handler.go, internal/interfaces/http/middleware/idempotency.go, cmd/server/main.go, config/{config.go, loader.go}, ansible/roles/server/defaults/main.yml, .gitlab-ci.yml, sdk/js/src/services/{wallet, billing, commerce, earnings}-service.js, sdk/js/scripts/run-stripe-wallet-certification.mjs.
GitLab API — GET /projects/304/merge_requests?source_branch=codex/r0-soft-financial-dev-merge&state=all (zero results), GET /projects/304/pipelines?ref=codex/r0-soft-financial-dev-merge (19433 success 2026-05-12).
ArgoCD repo gitlab.avvyland.com/avvy/argocd — state-server-all/overlays/prod/patch-deployment.yaml (pinned to :latest).
avvy-app — src/components/dashboard/content/wallet.jsx (the R0-aligned UI).
DomiDo PayDo (separate product line, archival): GitLab group gitlab.avvyland.com/domido — paydo* repos with 2018–2019 timestamps; no inheritance to Avvyland R0.
Master-task-spec snapshot state-server-master-task-spec-2026-04-20/ — locally; docs/FINANCIAL_BACKEND.md is byte-identical to the R0 branch copy; serves as a frozen design + release-note record.