Manage billing, track usage, and monitor rate limits for Better Hub’s AI features and GitHub API calls.
GET /api/billing/balance
Get the user’s current credit balance and expiration information.
Response
Available credits (USD equivalent)
Total credits granted to user (USD equivalent)
ISO 8601 timestamp of the nearest credit expirationReturns null if no credits are expiring
Whether user has received welcome credits
const response = await fetch('/api/billing/balance');
const balance = await response.json();
console.log(balance);
/*
{
available: 5.75,
totalGranted: 10.00,
nearestExpiry: "2024-02-15T00:00:00Z",
welcomed: true
}
*/
Credits may have expiration dates. Check nearestExpiry to warn users about expiring credits.
GET /api/billing/spending-limit
Get current spending limit configuration and usage for the billing period.
Response
Credit Mode (when user has credits):
Total granted credits (USD)
Monthly spending cap (USD)Default: 10.00
Current period usage (USD)
Period start date (ISO 8601)
Subscription Mode (when user has subscription):
Billing mode: "subscription"
Monthly spending cap (USD)
Current period usage (USD)
Subscription period start (ISO 8601)
Remaining budget for current period (USD)
const response = await fetch('/api/billing/spending-limit');
const limit = await response.json();
console.log(limit);
/*
{
mode: "credit",
available: 5.75,
totalGranted: 10.00,
monthlyCapUsd: 10.00,
periodUsageUsd: 3.25,
periodStart: "2024-01-01T00:00:00Z"
}
*/
PATCH /api/billing/spending-limit
Update the monthly spending cap.
New monthly cap in USDSet to null to remove cap (unlimited)
Response
Updated monthly cap value
const response = await fetch('/api/billing/spending-limit', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
monthlyCapUsd: 25.00
})
});
const updated = await response.json();
console.log('New cap:', updated.monthlyCapUsd); // 25.00
Spending caps protect against unexpected charges. Set a reasonable cap based on your expected usage.
GET /api/rate-limit
Get current GitHub API rate limit status.
Response
Maximum requests allowed per hour
Requests remaining in current window
Requests used in current window
Unix timestamp (seconds) when rate limit resets
const response = await fetch('/api/rate-limit');
const rateLimit = await response.json();
console.log(rateLimit);
/*
{
limit: 5000,
remaining: 4847,
used: 153,
resetAt: 1705329600
}
*/
Understanding Rate Limits
GitHub API Limits
Authenticated requests have different limits based on the auth method:
- OAuth Token: 5,000 requests/hour
- Personal Access Token: 5,000 requests/hour
- Unauthenticated: 60 requests/hour
AI Usage Limits
AI features are rate limited by:
- Monthly Spending Cap: Configurable via
/api/billing/spending-limit
- Credit Balance: Available credits via
/api/billing/balance
- Per-request Costs: Varies by model and token usage
Common Rate Limit Errors
GitHub API (429):
{
"error": "API rate limit exceeded",
"resetAt": 1705329600
}
AI Usage (429):
{
"error": "RATE_LIMIT_EXCEEDED",
"allowed": false,
"reason": "rate_limit",
"periodUsageUsd": 15.50,
"monthlyCapUsd": 10.00
}
Billing Error Codes
Error Types
RATE_LIMIT_EXCEEDED - Monthly spending cap reached
INSUFFICIENT_CREDITS - No credits remaining
SUBSCRIPTION_REQUIRED - Feature requires paid plan
INVALID_API_KEY - Custom API key invalid
Handling Errors
async function handleAIRequest(requestFn: () => Promise<Response>) {
try {
const response = await requestFn();
if (response.status === 429) {
const error = await response.json();
switch (error.error) {
case 'RATE_LIMIT_EXCEEDED':
return {
success: false,
message: `Monthly spending cap reached ($${error.monthlyCapUsd}). ` +
`Current usage: $${error.periodUsageUsd}`,
canRetry: false
};
case 'INSUFFICIENT_CREDITS':
return {
success: false,
message: 'No credits remaining. Please purchase more credits.',
canRetry: false
};
default:
return {
success: false,
message: 'Rate limit exceeded. Please try again later.',
canRetry: true
};
}
}
return { success: true, response };
} catch (error) {
return {
success: false,
message: 'Request failed',
canRetry: true
};
}
}
Usage Tracking
Monitor Spending
interface UsageStats {
currentPeriod: {
used: number;
cap: number;
remaining: number;
percentUsed: number;
};
credits: {
available: number;
total: number;
expiringSoon: boolean;
};
github: {
remaining: number;
limit: number;
percentUsed: number;
};
}
async function getUsageStats(): Promise<UsageStats> {
const [spending, balance, rateLimit] = await Promise.all([
fetch('/api/billing/spending-limit').then(r => r.json()),
fetch('/api/billing/balance').then(r => r.json()),
fetch('/api/rate-limit').then(r => r.json())
]);
const now = new Date();
const nearExpiry = balance.nearestExpiry
? new Date(balance.nearestExpiry)
: null;
const daysUntilExpiry = nearExpiry
? Math.ceil((nearExpiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
: Infinity;
return {
currentPeriod: {
used: spending.periodUsageUsd,
cap: spending.monthlyCapUsd,
remaining: spending.mode === 'subscription'
? spending.remainingUsd
: spending.available,
percentUsed: (spending.periodUsageUsd / spending.monthlyCapUsd) * 100
},
credits: {
available: balance.available,
total: balance.totalGranted,
expiringSoon: daysUntilExpiry <= 7
},
github: {
remaining: rateLimit.remaining,
limit: rateLimit.limit,
percentUsed: (rateLimit.used / rateLimit.limit) * 100
}
};
}
// Display usage dashboard
const stats = await getUsageStats();
console.log('📊 Usage Statistics:');
console.log(`AI: $${stats.currentPeriod.used.toFixed(2)}/$${stats.currentPeriod.cap.toFixed(2)} (${stats.currentPeriod.percentUsed.toFixed(1)}%)`);
console.log(`Credits: $${stats.credits.available.toFixed(2)} available`);
console.log(`GitHub API: ${stats.github.remaining}/${stats.github.limit} requests remaining`);
if (stats.credits.expiringSoon) {
console.warn('⚠️ Credits expiring soon!');
}