
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.
Viewing readonly version of main branch: v122View latest version
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 extraction
- 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