
Unlisted
Like
readback-api
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
This backend provides a speech synthesis API with comprehensive usage tracking, multi-tier subscriptions, and non-expiring credit packs.
index.ts- Main Hono app with route definitions and webhook endpointsmiddleware/auth.ts- User identification, authentication, and plan detection (RevenueCat + admin key)middleware/usage.ts- Dual-bucket usage tracking, allocation, and rate limitingdatabase/usage.ts- Monthly usage tracking operationsdatabase/credits.ts- Non-expiring credits ledger systemplans/config.ts- Central configuration for plans and credit packs
- Environment is configured per-request, not globally
- Clients can specify environment using:
- Header:
X-Environment: SANDBOXorX-Environment: PRODUCTION - Query Parameter:
?environment=sandboxor?environment=production - Default: PRODUCTION (when not specified)
- Header:
- Sandbox subscriptions and credits are completely isolated from production
- All webhook events are stored with their actual environment from RevenueCat
- Queries filter based on the request's environment parameter
- Admin access via
ADMIN_ACCESS_KEYenvironment variable - Customer authentication via RevenueCat subscription verification
- Authorization header format:
Bearer {customer_id}orCustomer {customer_id} - Automatic plan detection (Plus vs Pro) from RevenueCat entitlements
- Subscription filtering based on per-request environment parameter
- Plus Plan: 900,000 characters per calendar month
- Pro Plan: 2,700,000 characters per calendar month
- Calendar month-based tracking (resets on 1st of each month)
- Subscription credits expire at the end of each month
- Initial Free Grant: 45,000 characters for all users
- Credit Packs: Purchasable packs of 25k, 100k, or 500k characters
- Credits never expire and accumulate across purchases
- Used only after subscription credits are exhausted
- Subscription credits are always used first (if available)
- Non-expiring credits are used when subscription is exhausted or unavailable
- Single requests can consume from both buckets if needed
- Requests are rejected when both buckets are empty
Tracks all character usage:
id- Auto-incrementing primary keycustomer_id- Customer identifier from RevenueCatcharacter_count- Number of characters in the requestrequest_timestamp- ISO timestamp of the requestcreated_at- Database insertion timestamp
Ledger of all credit additions:
id- Auto-incrementing primary keycustomer_id- Customer identifiersource- Type of grant (free_grant, iap, admin, refund)product_lookup_key- RevenueCat product ID for purchasesrevenuecat_tx_id- Transaction ID for idempotencycharacters_granted- Number of characters granted (negative for refunds)price_usd- Price paid for the creditsgranted_at- Timestamp of grantenvironment- Environment of the grant (SANDBOX or PRODUCTION)
Ledger of non-expiring credit consumption:
id- Auto-incrementing primary keycustomer_id- Customer identifierusage_id- Foreign key to customer_usage_v1characters_consumed- Number of non-expiring characters usedconsumed_at- Timestamp of consumption
Converts text to speech using LemonFox API.
Headers:
Authorization: Bearer {customer_id}orAuthorization: Bearer {admin_key}X-Environment: SANDBOX(optional, defaults to PRODUCTION)
Query Parameters:
environment=sandbox(optional, alternative to header)
Body:
{ "voice": "voice_name", "input": "text to convert" }
Response:
{ "audio": "base64_encoded_audio", "word_timestamps": [ { "word": "hello", "start": 0.0, "end": 0.5 } ] }
Get comprehensive usage statistics and credit balances.
Headers:
Authorization: Bearer {customer_id}orAuthorization: Customer {customer_id}X-Environment: SANDBOX(optional, defaults to PRODUCTION)
Query Parameters:
environment=sandbox(optional, alternative to header)
Response (Plus/Pro users):
{ "user_tier": "premium", "monthly_limit": 900000, "current_usage": 150000, "remaining_characters": 750000, "usage_percentage": 17, "reset_date": "2024-02-01T00:00:00.000Z", "plan_renewal_date": "February 5, 2024", "plan_term": "monthly", "plan_term_in_days": 30, "plan_key": "plus", "plan_gross_cost": 2.99, "trialing": false, "credits": { "subscription": { "plan_key": "plus", "monthly_limit": 900000, "current_usage": 150000, "remaining_characters": 750000, "usage_percentage": 17, "reset_date": "2024-02-01T00:00:00.000Z" }, "non_expiring_tokens": { "balance": 70000, "total_granted": 95000, "total_consumed": 25000, "purchases": [ { "source": "free_grant", "characters": 45000, "price_usd": 0 }, { "source": "iap", "product_lookup_key": "credit_pack_1hr", "characters": 25000, "price_usd": 2.99 } ] } } }
Response (Free users):
{ "user_tier": "free", "lifetime_limit": 45000, "current_usage": 15000, "remaining_characters": 30000, "usage_percentage": 33, "message": "Subscribe for monthly credits or purchase additional credits.", "plan_renewal_date": null, "plan_term": null, "plan_term_in_days": null, "plan_key": null, "plan_gross_cost": null, "trialing": false, "credits": { "subscription": null, "non_expiring_tokens": { "balance": 30000, "total_granted": 45000, "total_consumed": 15000, "purchases": [ { "source": "free_grant", "characters": 45000, "price_usd": 0 } ] } } }
Webhook endpoint for RevenueCat to notify about IAP purchases and refunds.
Headers:
Authorization: {REVENUECAT_WEBHOOK_AUTH}- Must match environment variable
Handled Events:
NON_RENEWING_PURCHASE- Grants credits for credit pack purchasesREFUND- Deducts credits for refunded purchases (negative grant)
Environment Handling:
- Webhook processes ALL events from RevenueCat (both sandbox and production)
- Each event's environment (SANDBOX/PRODUCTION) is stored with the grant/event
- Credits are filtered at query time based on the request's environment parameter
Response:
{ "success": true }
Get current credit balance.
Headers:
Authorization: Bearer {customer_id}X-Environment: SANDBOX(optional, defaults to PRODUCTION)
Query Parameters:
environment=sandbox(optional, alternative to header)
Response:
{ "customer_id": "customer_123", "balance": 45000, "total_granted": 45000, "total_consumed": 0 }
Health check endpoint.
Response:
{ "status": "ok", "timestamp": "2024-01-01T00:00:00.000Z" }
ADMIN_ACCESS_KEY- Admin authentication keyREVENUECAT_API_KEY- RevenueCat API key for subscription verificationREVENUECAT_WEBHOOK_AUTH- Static auth key for webhook endpointLEMONFOX_API_KEY- LemonFox API key for speech synthesisPDFVECTOR_API_KEY- PDFVector API key for PDF extractionPDF_PARSER_API_KEY- Admin API key for the primary OCR PDF endpoint
- Plus: 900,000 characters per calendar month
- Pro: 2,700,000 characters per calendar month
- Initial grant: 45,000 characters (free for all users)
- Purchasable packs: 25k, 100k, 500k characters
- Never expire, used after subscription credits
- Admin users have unlimited usage
- Requests exceeding available credits are rejected with HTTP 429
- Usage is recorded after successful requests only
400- Bad request (no input text provided)401- Missing or invalid authorization403- No active subscription or subscription verification failed429- Character limit exceeded (both subscription and non-expiring credits exhausted)500- Internal server error
- RevenueCat webhook uses transaction IDs to prevent double-crediting
- Credit grants with the same
revenuecat_tx_idare ignored (INSERT OR IGNORE)
- New tables created without modifying existing
customer_usage_v1 - Initial 45k grant applied on first interaction with new system
- No historical data migration - clean slate approach
- Calculate available subscription credits (capped at plan limit)
- Calculate available non-expiring credits from ledger
- Allocate request: subscription first, then non-expiring
- Record usage in
customer_usage_v1for all usage - Record debit in
credit_debits_v1for non-expiring portion only