Last Updated: 2026-06-28 Status: Active Companion docs: architecture.md · database.md · api-spec.md
1. Rate
0.25 credits per accepted (non-spam) submission.
Spam submissions (honeypot trips, captcha failures) are stored but not billed. The credit charge happens only after the submission passes all spam guards and is stored with status=new.
2. Pre-flight Credit Gate
Before storing a submission, the service checks the tenant's available credit balance:
(balance × 1000) - accumulator_thousandths ≥ 250balance= sum of active subscription credits + non-expired permanent credits (fromtenant_creditsin core DB)accumulator_thousandths= unbilled thousandths already committed but not yet settled (fromtenant_forms_usagein forms DB)250= the cost of one submission in thousandths (0.25 × 1000)
If the gate fails → 402 Payment Required — the submission is rejected and nothing is stored.
NOTE
The credit balance is read from CORE_DATABASE_URL. If this connection is unavailable (network hiccup, DB down), the gate fails open — the submission is accepted and metered normally. The metering step will then fail silently and log an error. This is a deliberate availability trade-off (capture > billing accuracy during infra wobble).
3. Accumulator Model
Credits are billed in whole units, but submissions cost 0.25 credits each. To avoid fractional credit charges, the service uses an accumulator in tenant_forms_usage:
Every 4 submissions = 1 credit charged to the core ledger.
Settlement
When accumulator_thousandths + 250 ≥ 1000, the service:
- Calculates whole credits to charge:
Math.floor((accumulator + 250) / 1000) - Debits the core ledger: inserts a
credit_transactionsrow (reason="forms.submission") - Updates
accumulator_thousandthsto the remainder:(accumulator + 250) % 1000 - Increments
settlement_seq— an optimistic lock to prevent double-charges if two submissions arrive simultaneously
Settlement runs in waitUntil() and does not block the visitor's response.
4. System Tenant Bypass
Tenants with the system:owner role bypass all credit checks — no pre-flight gate and no metering. This is used for internal tooling and demo accounts.
5. Analytics
Credit usage is surfaced in the tenant overview analytics (GET /admin/forms/overview):
{
"credits": {
"balance": 42.5,
"spentThisMonth": 21.75,
"perSubmission": 0.25,
"pending": 500
}
}| Field | Meaning |
|---|---|
balance |
Current credit balance (subscription + permanent, minus expired) |
spentThisMonth |
Sum of credit_transactions debits with reason="forms.submission" since the 1st of the current UTC month |
perSubmission |
Always 0.25 — the cost per submission |
pending |
Unbilled thousandths in tenant_forms_usage.accumulator_thousandths (0–999). Divide by 1000 to get pending credits. |
6. Plan-based Monthly Cap
The core DB stores a monthly submission limit per plan in plan_service_limits. The forms service reads this limit and surfaces it in the overview (usage.limit). However, this cap is not currently enforced in the public submit path — the credit balance is the hard cap. A -1 limit means no cap is configured for the tenant's plan.
7. Known Behaviors
| Behavior | Detail |
|---|---|
| Fail-open on core DB outage | Credit gate skipped if CORE_DATABASE_URL is unreachable. Submissions are accepted and logged, but metering may not settle. |
| Spam not billed | Honeypot and captcha failures bypass billing entirely. |
| Settlement idempotency | settlement_seq prevents double-charges if two submissions race through the accumulator simultaneously. |
| No refunds on delete | Deleting a submission does not refund the credit already charged. |