1. Overview
| Component | Role |
|---|---|
| RollingCreditsService | generate_subscription_credits() — prorated allocation, DB (CreditBalance, CreditTransaction), optional blockchain registration |
| SubscriptionService | create_subscription (activate) and renew_subscription → call RollingCreditsService.generate_subscription_credits for pro / premium |
| CreditToken.sol | ERC-721 NFT: one token per user, CreditBalanceStruct with 14 credit types; mintCredits, updateCredits (4 decimals: 10000 = 1.0) |
| BlockchainService | Loads CreditToken ABI, mint_credit_token(), update_credit_token() |
app/services/rolling_credits_service.py, app/services/subscription_service.py, app/services/blockchain_service.py, contracts/contracts/CreditToken.sol
2. Configuration
| Variable | Description | Default |
|---|---|---|
CREDIT_TOKEN_CONTRACT | CreditToken (ERC-721) contract address. If empty, credits are still generated in DB but not registered on-chain. | "" |
X402_NETWORK_RPC_URL | RPC for the chain where CreditToken is deployed (Base, Base Sepolia, or local Hardhat). Must match deployment. | https://mainnet.base.org |
BLOCKCHAIN_DEPLOYER_PRIVATE_KEY | Private key of the account that deploys and is owner of CreditToken. Required for mintCredits / updateCredits (both are onlyOwner). | — |
wallet_address set. If missing, credits are stored in DB only (blockchain_registered=false).
3. Tier Allocation and Proration
| Tier | Example (full month) |
|---|---|
| pro | signing 20, document_review 20, verification 20, trading 30, loaning 20, borrowing 20, compliance_check 20, securitization 15, risk_analysis 20, quantitative_analysis 15, stock_prediction_* 5 each, universal 100 |
| premium | ~3× pro (e.g. universal 300, stock_prediction_* 15 each) |
- Proration:
min(1.0, billing_days / 30). For a 15-day period, allocation is halved. - Activate: Billing window =
[started_at, expires_at]or[started_at, started_at+30]for lifetime. - Renew: Billing window =
[expires_at, expires_at+30](monthly) or[expires_at, expires_at+365](yearly).
4. Deploying CreditToken
CreditToken is deployed by the samedeploy.js as the securitization contracts. Use the same network (local, Base Sepolia, or Base mainnet) and the same PRIVATE_KEY / BLOCKCHAIN_DEPLOYER_PRIVATE_KEY.
Compile
Deploy (examples)
Local Hardhat (persistent node):.env:
X402_NETWORK_RPC_URL matches the deployment network (e.g. http://127.0.0.1:8545, https://sepolia.base.org, https://mainnet.base.org).
5. Artifacts and ABI
BlockchainService loads the CreditToken ABI from:
npx hardhat compile. If it is missing, mint_credit_token / update_credit_token are skipped ("CreditToken ABI not loaded"). Credits are still written to the database.
6. Flows
Activate (create_subscription)
SubscriptionService.create_subscription(...)creates aUserSubscriptionand flushes.- For
tierin("pro","premium"), callsRollingCreditsService.generate_subscription_credits(user_id, subscription.id, tier, period_start, period_end). generate_subscription_creditsupdates or createsCreditBalance, addsCreditTransactionrows, then_register_credits_on_blockchainifCREDIT_TOKEN_CONTRACTis set and the user haswallet_address.
Renew (renew_subscription)
SubscriptionService.renew_subscription(subscription_id)loads the subscription, computesperiod_start=expires_atandperiod_end=period_start+30or+365.- For
pro/premium, callsgenerate_subscription_creditsfor that window. - Sets
subscription.expires_at = period_endand commits.
Blockchain registration (_register_credits_on_blockchain)
- No
CREDIT_TOKEN_CONTRACTor no RPC/deployer:registered: false,reason: "blockchain_not_configured"or"blockchain_not_connected". - User has no
wallet_address:registered: false,reason: "no_wallet". - First run (no
blockchain_token_id):mint_credit_token(user_address, struct_from(generated_credits)); on success,blockchain_token_id,blockchain_tx_hash,blockchain_chain_idare stored onCreditBalance. - Later runs (has
blockchain_token_id):update_credit_token(token_id, credit_type, amount, is_spend=False)for each type withamount > 0.
7. Troubleshooting
| Symptom | Checks |
|---|---|
| No credits in DB on activate | Tier must be pro or premium; create_subscription must complete without raising before commit. Check logs for "Rolling credits generated on activate" or "Rolling credits on activate failed". |
Credits in DB but blockchain_registered=false | CREDIT_TOKEN_CONTRACT set? X402_NETWORK_RPC_URL correct and RPC reachable? User wallet_address present? BlockchainService deployer (owner) and credit_token ABI loaded? Check blockchain object in generate_subscription_credits result (reason). |
| ”CreditToken ABI not loaded” | Run npx hardhat compile in contracts/ and ensure contracts/artifacts/contracts/CreditToken.sol/CreditToken.json exists. |
| ”blockchain_not_configured” | Set CREDIT_TOKEN_CONTRACT in .env. |
| ”no_wallet” | User needs wallet_address (or a resolved wallet from your auth/wallet flow). |
| Mint/update revert | Deployer must be owner of CreditToken. For updateCredits, on-chain balance for that type must be ≥ amount when isSpend=true (only isSpend=false is used for subscription credits). |
8. Plaid and credits (deduct per call)
Each Plaid API call (accounts, balances, transactions, etc.) deducts 1 trading credit per request. The equivalent cost is configurable and typically 0.10 per call (seePLAID_COST_USD in app/core/config.py, default 0.05). This cost is used when returning 402 Payment Required so the client can show the correct pay-as-you-go amount.
- Deduct: 1 trading credit per Plaid call (e.g.
/api/banking/accounts,/api/banking/balances,/api/banking/transactions). - Tracking:
PlaidUsageTrackingrecords each call (endpoint, item_id, optional cost_usd) for billing and analytics. - Config:
PLAID_COST_USD(default0.05) is used as the USD amount in 402 responses when credits are insufficient.
9. References
- Contracts:
contracts/README.md,contracts/scripts/deploy.js - Config:
app/core/config.py(CREDIT_TOKEN_CONTRACT),dev/environment.md - Models:
app/db/models.py(CreditBalance,CreditTransaction,CreditType)