Skip to content

feat(billing): add Stripe Test Mode Credits recharge - #344

Open
MrParamecium wants to merge 14 commits into
mainfrom
feat/credits-recharge-design
Open

feat(billing): add Stripe Test Mode Credits recharge#344
MrParamecium wants to merge 14 commits into
mainfrom
feat/credits-recharge-design

Conversation

@MrParamecium

Copy link
Copy Markdown
Collaborator

Summary

  • add a Stripe Test Mode-only Credits purchase flow with fixed and custom USD amounts
  • add immutable Credits ledgers and payment/refund persistence for SQLite and Postgres
  • add authenticated balance, ledger, Checkout status, webhook, and admin refund APIs
  • add a responsive Credits & Billing page with bounded payment polling and admin partial refunds
  • document local Stripe CLI setup, Sandbox testing, and the explicit Live Mode gate

Safety and scope

  • Stripe Live Mode keys are rejected
  • Checkout redirects never grant Credits; only verified Stripe webhooks can change balances
  • webhook replays, checkout retries, and refund retries are idempotent
  • Credits are ATL platform-service units, not simulated portfolio cash, stored money, securities, or withdrawable value
  • this PR does not implement Credit consumption, signup grants, referral rewards, expiration, or self-service refunds
  • no Stripe secret, webhook payload, local database, or card data is committed

Verification

  • 2805 passed, 72 skipped in the full backend suite before the final main sync
  • 174 passed, 6 skipped in the focused Credits/API/security/integration suite after syncing the latest main
  • 171 passed, 2 skipped in the packaging SDK suite after syncing the latest main
  • desktop and 375px mobile browser QA completed
  • real Stripe Sandbox acceptance completed:
    • $10 Checkout payment produced exactly one +10.00 ledger entry
    • Stripe CLI forwarded the signed checkout.session.completed webhook with HTTP 200
    • a $4 admin partial refund produced exactly one -4.00 ledger entry
    • Stripe CLI forwarded refund.created and refund.updated with HTTP 200
    • the final balance was exactly 6.00 Credits and the order was Partially Refunded

@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agentic-trading-lab Ready Ready Preview Aug 13, 2026 5:24pm

Comment thread dashboard/backend/tests/test_credits_frontend.py Fixed
Comment thread dashboard/backend/tests/test_credits_api.py Fixed
Comment thread dashboard/backend/tests/test_credits_api.py Fixed
Comment thread dashboard/backend/tests/integration/test_credits_checkout_flow.py Fixed
Comment thread dashboard/backend/tests/integration/test_credits_checkout_flow.py Fixed
Comment thread dashboard/backend/tests/domain/credits/test_repository_postgres.py Fixed
Comment thread dashboard/backend/tests/domain/credits/test_repository_postgres.py Fixed
Comment thread dashboard/backend/tests/domain/credits/test_repository_postgres.py Fixed
Comment thread dashboard/backend/domain/credits/repository_postgres.py Fixed
Comment thread dashboard/backend/domain/credits/repository.py Fixed
@FlyM1ss

FlyM1ss commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Heads-up from #348 — this branch's app.js cache buster is below main's.

main serves app.js?v=88; this PR ships 78, a number that already shipped. This PR does change app.js, so returning browsers would keep serving their cached copy. (styles.css?v=88 is fine — it clears main's 87.)

CI is green because test_cache_busters_bumped was updated to assert 78 to match. That guard only checks app.html against the test file and never against main (#347), so matching it to the shipped value hides exactly this case rather than catching it.

Please raise app.js above whatever main serves at merge time in both files — #341 is currently on 98/97, so re-check the floor. This PR is also showing mergeable: false right now, so app.html will need a manual resolution anyway; a buster conflict should always resolve upward, never to either side.

…-design

# Conflicts:
#	dashboard/backend/tests/test_frontend_fast_boot.py
#	dashboard/frontend/app.html
@MrParamecium

Copy link
Copy Markdown
Collaborator Author

Thanks for catching this. I merged the latest main, resolved the app.html and cache-buster test conflicts upward, and preserved the Credits script load order.

The branch now ships:

  • app.js?v=99
  • styles.css?v=98

Local verification after the merge:

  • Backend: 2873 passed, 74 skipped
  • Packaging: 171 passed, 2 skipped
  • Focused frontend/Credits: 123 passed, 6 skipped

The fix is in e2364db.

FlyM1ss added a commit to Allan-Feng/AgenticTrading that referenced this pull request Aug 14, 2026
Security review fixes for Open-Finance-Lab#341. The admin routes themselves held up — one
correct `require_admin` gate, a transactional PATCH, a constant-time compare,
and both twins serializing the one-shot promotion. What did not hold up was the
plane around them: the entitlement was built as a control and wired where it
could not bind, and bootstrap's own budgets could be turned on the operator.

- Close the two cap bypasses. `resolve_owner_cap_context` returned None the
  instant `owner_user_id` was falsy, and agent creation needs no login at all
  (`_require_owner_context` accepts a self-chosen X-Session-Id), so signing in
  was strictly worse than staying anonymous: one shared account budget versus
  the per-agent cap times however many agents you registered. Unclaimed agents
  are now billed to the browser session that created them, resolved inside the
  store (`list_owner_scope_agent_ids`, both twins) because `_public_agent`
  withholds `owner_browser_session` on purpose — `_owner_context` accepts that
  value AS an ownership credential.
- Cap the legacy `/api/v1/backtest/*` surface. It authenticates nothing and
  writes no `protocol_runs` row, so all three protocol caps were blind to it:
  a bare POST spawned a thread and pinned a bar window, unbounded, beside a
  quota system holding every authenticated agent to its limit. Per-session and
  global budgets, counted and inserted under one lock. Opt-in, so the protocol
  path (already capped three ways) is not charged twice.
- Raise the default quota to 5 and make it env-overridable. Nothing seeds
  `user_entitlements` at signup and nothing backfills, so `1` was not a default
  but every existing account's live limit on deploy day — a silent demotion
  from 5x(agents), with no remedy short of an admin.
- Bootstrap: check the global budget AFTER the compare. Checked first, 20 wrong
  guesses a window — re-spent every window, from any account, and signup is
  open — refused the correct secret indefinitely, and the window it blocked is
  exactly the fresh-deploy window the route exists for. Require >= 32 chars
  (refused as if unset), and answer 403 for unset/weak/wrong alike so a caller
  cannot learn whether a deployment is bootstrappable.
- Lock FixedWindowRateLimiter. The module had no `threading` import at all, and
  the buckets are mutated on every call — `_pruned` popleft()s from inside
  `check`, not only `record`. `allow()` is now atomic.
- Reject control characters in emails at the validator. strip() only touched
  the ends, so an interior newline validated, was stored, and forged lines in
  the admin console's role-change confirm() — a native dialog with no markup
  to escape. One fix at the source beats one guard per renderer.
- Get the store off the event loop in signup/login. This PR added a sync
  `get_entitlements()` to `_auth_json`, called from both async handlers, which
  the threadpool guard cannot see (it pins plain-`def` routes, and those two
  are exempt for already awaiting). Bundled with `create_session` into one
  `asyncio.to_thread` hop, plus the guard that can see it.

Also: router-level `require_admin` so a route added later is gated by default,
a 403 case for all four routes enumerated from the router, an audit line on
every role/quota change (ids only — no email reaches a print sink), quota floor
0 so a quota can actually suspend, `credits` labelled not-enforced in the
console it is editable from, and the first Postgres tests for
`promote_first_admin` — prod's copy of the one-shot predicate had none.

Cache busters to 100/100: Open-Finance-Lab#342 also ships styles.css?v=97 and Open-Finance-Lab#344 ships 99/98,
so 98/97 would have collided with no git conflict (Open-Finance-Lab#347/Open-Finance-Lab#348).

Backend suite: 2797 passed, 76 skipped. SDK suite: 171 passed, 2 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCiPGs3ZYVjyFYVLa6E7bZ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants