Enhance billing and credits management in the application

- Added new commands in `package.json` for building macOS x64 applications.
- Updated `TOOLS.md` to reflect the latest tool statistics and improved formatting.
- Refactored `BillingPanel` to include credit balance and team usage state management, with loading indicators and top-up functionality.
- Integrated budget usage tracking in `Conversations` page, displaying budget status and a top-up call-to-action when funds are depleted.
- Updated `creditsApi` to include new endpoints for fetching team usage and handling credit top-ups, with improved data structures for credit balance and transactions.
- Simplified onboarding steps by removing the invite code step and adjusting the animation sequence.

These changes improve user experience by providing better visibility into billing and usage, as well as streamlining the onboarding process.
This commit is contained in:
M3gA-Mind
2026-03-16 18:10:57 +05:30
parent 03b213e3bb
commit bf0f8e6da9
6 changed files with 299 additions and 99 deletions
+20 -28
View File
@@ -7,7 +7,6 @@ This document lists all available tools that AlphaHuman can use to interact with
AlphaHuman has access to **4 tools** across **3 integrations** organized into **6 categories**.
**Quick Statistics:**
- **Telegram**: 2 tools
- **Notion**: 1 tools
- **Gmail**: 1 tools
@@ -74,7 +73,6 @@ This skill provides 1 tool for gmail integration.
**Description**: Send an email via Gmail
**Parameters**:
- **body** (string) **(required)**: Email body content
- **subject** (string) **(required)**: Email subject line
- **to** (string) **(required)**: Recipient email address
@@ -82,11 +80,14 @@ This skill provides 1 tool for gmail integration.
**Usage Context**: Available in all environments
**Example**:
```json
{
"tool": "send_email",
"parameters": { "body": "example_body", "subject": "example_subject", "to": "example_to" }
"parameters": {
"body": "example_body",
"subject": "example_subject",
"to": "example_to"
}
}
```
@@ -103,7 +104,6 @@ This skill provides 1 tool for notion integration.
**Description**: Create a new page in Notion workspace
**Parameters**:
- **content** (array): Page content blocks
- **parent_id** (string) **(required)**: Parent database or page ID
- **title** (string) **(required)**: Page title
@@ -111,11 +111,14 @@ This skill provides 1 tool for notion integration.
**Usage Context**: Available in all environments
**Example**:
```json
{
"tool": "create_page",
"parameters": { "content": [], "parent_id": "example_parent_id", "title": "example_title" }
"parameters": {
"content": [],
"parent_id": "example_parent_id",
"title": "example_title"
}
}
```
@@ -132,7 +135,6 @@ This skill provides 2 tools for telegram integration.
**Description**: Send a message to a Telegram chat or user
**Parameters**:
- **chat_id** (string) **(required)**: Telegram chat ID or username
- **message** (string) **(required)**: Message text to send
- **parse_mode** (string): Message formatting mode
@@ -140,7 +142,6 @@ This skill provides 2 tools for telegram integration.
**Usage Context**: Available in all environments
**Example**:
```json
{
"tool": "send_message",
@@ -159,16 +160,20 @@ This skill provides 2 tools for telegram integration.
**Description**: Retrieve message history from a Telegram chat
**Parameters**:
- **chat_id** (string) **(required)**: Telegram chat ID or username
- **limit** (number): Number of messages to retrieve
**Usage Context**: Available in all environments
**Example**:
```json
{ "tool": "get_chat_history", "parameters": { "chat_id": "example_chat_id", "limit": 10 } }
{
"tool": "get_chat_history",
"parameters": {
"chat_id": "example_chat_id",
"limit": 10
}
}
```
---
@@ -176,42 +181,36 @@ This skill provides 2 tools for telegram integration.
## Tool Usage Guidelines
### Authentication
- All tools require proper authentication setup through the Skills system
- OAuth credentials are managed securely and refreshed automatically
- API keys are stored encrypted in the application keychain
- Test credentials are available for development and testing environments
### Rate Limiting
- Tools automatically respect API rate limits of external services
- Intelligent retry logic handles temporary failures with exponential backoff
- Bulk operations are automatically chunked to avoid hitting limits
- Rate limit status is monitored and reported in real-time
### Error Handling
- All tools return structured error responses with detailed information
- Network failures trigger automatic retry with configurable attempts
- Invalid parameters return clear validation messages with examples
- Tool execution timeouts are handled gracefully with partial results
### Security & Privacy
- Input validation is performed on all parameters using JSON Schema
- Output sanitization prevents injection attacks and data leakage
- Sensitive data is never logged or exposed in error messages
- All API communications use secure protocols (HTTPS/TLS)
### Performance Optimization
- Tool results are cached when appropriate to reduce API calls
- Parallel execution is used for independent operations
- Connection pooling minimizes overhead for repeated API calls
- Background sync keeps data fresh without blocking operations
### Monitoring & Observability
- Tool execution metrics are collected for performance analysis
- Error rates and response times are monitored continuously
- Debug logging is available in development environments
@@ -230,21 +229,18 @@ Tools are provided by Skills, which are JavaScript modules running in a secure V
## Integration Architecture
### V8 Runtime
- Skills execute in isolated V8 JavaScript contexts on desktop platforms
- Mobile platforms use lightweight alternatives with server-side execution
- Memory limits and execution timeouts prevent resource exhaustion
- Inter-skill communication is managed through secure message passing
### API Bridge
- Tools communicate with external services through standardized API bridges
- Rate limiting, retry logic, and error handling are implemented at the bridge level
- Authentication tokens are managed centrally and shared across tools
- Response caching and optimization are handled transparently
### Data Flow
1. Tool request received from AI agent
2. Input validation and parameter processing
3. Skill execution in secure V8 context
@@ -255,21 +251,18 @@ Tools are provided by Skills, which are JavaScript modules running in a secure V
## Support & Troubleshooting
### Common Issues
1. **Tool Not Available**: Check if the associated skill is enabled in Settings → Skills
2. **Authentication Errors**: Verify credentials in the skill's configuration panel
3. **Rate Limit Exceeded**: Wait for the limit to reset or upgrade your API plan
4. **Invalid Parameters**: Review the parameter documentation and examples above
### Getting Help
- **Skill Documentation**: Each skill has detailed setup and usage instructions
- **Debug Logs**: Enable verbose logging in development mode for detailed error information
- **Community Support**: Join our Discord community for help from other users
- **Technical Support**: Contact our support team for critical issues
### Contributing
- **New Tools**: Submit tool requests through our GitHub repository
- **Bug Reports**: Report issues with specific tools and include error logs
- **Improvements**: Suggest enhancements to existing tools and their documentation
@@ -277,11 +270,10 @@ Tools are provided by Skills, which are JavaScript modules running in a secure V
---
**Tool Statistics**
- Total Tools: 4
- Active Skills: 3
- Categories: 6
- Last Updated: 2026-03-11T23:23:47.633Z
- Last Updated: 2026-03-16T12:17:45.377Z
_This file was automatically generated at build time from the V8 skills runtime._
_For the most up-to-date information, regenerate this file by running `yarn tools:generate`._
*This file was automatically generated at build time from the V8 skills runtime.*
*For the most up-to-date information, regenerate this file by running `yarn tools:generate`.*
+2
View File
@@ -14,6 +14,8 @@
"tauri": "tauri",
"tauri:dev": "RUST_LOG=debug source scripts/load-dotenv.sh 2>/dev/null; (cd src-tauri && bash scripts/download-tdlib.sh) && LOCAL_TDLIB_PATH=$(pwd)/src-tauri/tdlib-cache/macos-$(uname -m | sed 's/arm64/aarch64/; s/x86_64/x86_64/') tauri dev",
"macos:build:debug": "yarn macos:build:release --debug",
"macos:x64:build:debug": "yarn macos:x64:build:release --debug",
"macos:x64:build:release": "source scripts/load-dotenv.sh 2>/dev/null; (cd src-tauri && bash scripts/download-tdlib.sh) && LOCAL_TDLIB_PATH=$(pwd)/src-tauri/tdlib-cache/macos-x86_64 tauri build --target x86_64-apple-darwin --bundles app dmg",
"macos:build:release": "source scripts/load-dotenv.sh 2>/dev/null; (cd src-tauri && bash scripts/download-tdlib.sh) && LOCAL_TDLIB_PATH=$(pwd)/src-tauri/tdlib-cache/macos-$(uname -m | sed 's/arm64/aarch64/; s/x86_64/x86_64/') tauri build --bundles app dmg",
"macos:build:sign:release": "source scripts/load-env.sh && tauri build --bundles app dmg",
"macos:run": "open 'src-tauri/target/debug/bundle/macos/AlphaHuman.app'",
+130 -42
View File
@@ -1,6 +1,11 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { billingApi } from '../../../services/api/billingApi';
import {
type CreditBalance,
type TeamUsage,
creditsApi,
} from '../../../services/api/creditsApi';
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
import { fetchCurrentUser } from '../../../store/userSlice';
import type { PlanTier } from '../../../types/api';
@@ -31,7 +36,12 @@ const BillingPanel = () => {
const currentTier: PlanTier = activeTeam?.team.subscription?.plan ?? 'FREE';
const hasActive = activeTeam?.team.subscription?.hasActiveSubscription ?? false;
const planExpiry = activeTeam?.team.subscription?.planExpiry;
const usage = user?.usage;
// Credits & usage state
const [creditBalance, setCreditBalance] = useState<CreditBalance | null>(null);
const [teamUsage, setTeamUsage] = useState<TeamUsage | null>(null);
const [isLoadingCredits, setIsLoadingCredits] = useState(false);
const [isToppingUp, setIsToppingUp] = useState(false);
// Local state
const [billingInterval, setBillingInterval] = useState<'monthly' | 'annual'>('monthly');
@@ -43,9 +53,18 @@ const BillingPanel = () => {
const pollStartRef = useRef<number>(0);
const timeoutRef = useRef<number | null>(null);
// Fetch current plan on mount
// Fetch current plan, credits balance, and team usage on mount
useEffect(() => {
billingApi.getCurrentPlan().catch(console.error);
setIsLoadingCredits(true);
Promise.all([creditsApi.getBalance(), creditsApi.getTeamUsage()])
.then(([balance, usage]) => {
setCreditBalance(balance);
setTeamUsage(usage);
})
.catch(console.error)
.finally(() => setIsLoadingCredits(false));
}, []);
// When crypto is selected, force annual
@@ -102,7 +121,6 @@ const BillingPanel = () => {
currentTierRef.current = currentTier;
}, [currentTier]);
// eslint-disable-next-line react-hooks/preserve-manual-memoization
const startPolling = useCallback(() => {
if (pollRef.current) clearInterval(pollRef.current);
pollStartRef.current = Date.now();
@@ -162,6 +180,18 @@ const BillingPanel = () => {
}
};
const handleTopUp = async (amountUsd: number) => {
setIsToppingUp(true);
try {
const result = await creditsApi.topUp(amountUsd, 'stripe');
await openUrl(result.url);
} catch (err) {
console.error('Top-up failed:', err);
} finally {
setIsToppingUp(false);
}
};
// ── JSX ─────────────────────────────────────────────────────────────
return (
<div className="overflow-hidden flex flex-col">
@@ -174,40 +204,22 @@ const BillingPanel = () => {
{/* <div className="flex items-center justify-between max-w-md mx-auto"> */}
<div className="overflow-y-auto">
<div className="space-y-2">
<div className="max-w-md mt-4 mx-auto">
<div className="p-2.5">
<div className="max-w-md mt-4 mx-auto px-4 space-y-3">
{/* ── Current Plan Header ───────────────────────────────── */}
<div className="rounded-2xl border border-stone-700/50 bg-stone-800/40 p-3">
<div className="flex items-center justify-between mb-1.5">
<h3 className="text-sm font-semibold text-white">
Your Current Plan {currentTier}
Current Plan {currentTier}
</h3>
{usage && (
<span className="text-xs text-stone-400">
{Math.round((usage.spentThisCycleUsd / usage.cycleBudgetUsd) * 100)}% used
</span>
)}
</div>
{hasActive && (
<div className="flex items-center justify-between mb-1.5">
{planExpiry && (
<p className="text-xs text-stone-400">
Renews{' '}
{new Date(planExpiry).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})}
</p>
)}
{hasActive && (
<button
onClick={handleManageSubscription}
className="text-xs text-primary-400 hover:text-primary-300 font-medium transition-colors">
Manage Subscription
Manage
</button>
</div>
)}
{/* Renewal date (for non-active subscriptions) */}
{!hasActive && planExpiry && (
)}
</div>
{planExpiry && (
<p className="text-xs text-stone-400 mb-1.5">
Renews{' '}
{new Date(planExpiry).toLocaleDateString('en-US', {
@@ -217,20 +229,96 @@ const BillingPanel = () => {
})}
</p>
)}
{usage && (
<div className="h-1.5 bg-stone-700/60 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-300 bg-primary-500"
style={{
width: `${Math.min(
100,
(usage.spentThisCycleUsd / usage.cycleBudgetUsd) * 100
)}%`,
}}
/>
</div>
</div>
{/* ── Inference Budget (Team Usage) ─────────────────────── */}
<div className="rounded-2xl border border-stone-700/50 bg-stone-800/40 p-3">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-semibold text-white">Inference Budget</h3>
{isLoadingCredits && (
<span className="text-[10px] text-stone-500">Loading</span>
)}
{teamUsage && !isLoadingCredits && (
<span className="text-xs text-stone-400">
${teamUsage.remainingUsd.toFixed(2)} / ${teamUsage.cycleBudgetUsd.toFixed(2)} remaining
</span>
)}
</div>
{teamUsage ? (
<>
<div className="h-1.5 bg-stone-700/60 rounded-full overflow-hidden mb-2">
<div
className={`h-full rounded-full transition-all duration-300 ${
teamUsage.remainingUsd <= 0
? 'bg-coral-500'
: teamUsage.remainingUsd / teamUsage.cycleBudgetUsd < 0.2
? 'bg-amber-500'
: 'bg-primary-500'
}`}
style={{
width: `${Math.min(100, (teamUsage.remainingUsd / teamUsage.cycleBudgetUsd) * 100)}%`,
}}
/>
</div>
<div className="flex items-center justify-between">
<span className="text-[11px] text-stone-500">
Daily usage: ${teamUsage.dailyUsage.toFixed(3)}
</span>
<span className="text-[11px] text-stone-500">
{((teamUsage.totalInputTokensThisCycle + teamUsage.totalOutputTokensThisCycle) / 1000).toFixed(1)}k tokens this cycle
</span>
</div>
{teamUsage.remainingUsd <= 0 && (
<p className="text-[11px] text-coral-400 mt-1.5">
Budget exhausted top up your credits to continue using AI features.
</p>
)}
</>
) : isLoadingCredits ? (
<div className="h-1.5 w-full rounded-full bg-stone-700/60 animate-pulse" />
) : (
<p className="text-xs text-stone-500">Unable to load usage data</p>
)}
</div>
{/* ── Credits Balance & Top-up ──────────────────────────── */}
<div className="rounded-2xl border border-stone-700/50 bg-stone-800/40 p-3">
<h3 className="text-sm font-semibold text-white mb-2">Credits Balance</h3>
{creditBalance ? (
<div className="space-y-1.5 mb-3">
<div className="flex items-center justify-between">
<span className="text-xs text-stone-400">General credits</span>
<span className="text-xs font-medium text-white">
${creditBalance.balanceUsd.toFixed(2)}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xs text-stone-400">Top-up credits</span>
<span className="text-xs font-medium text-white">
${creditBalance.topUpBalanceUsd.toFixed(2)}
</span>
</div>
</div>
) : isLoadingCredits ? (
<div className="space-y-1.5 mb-3">
<div className="h-3 w-full rounded bg-stone-700/60 animate-pulse" />
<div className="h-3 w-3/4 rounded bg-stone-700/60 animate-pulse" />
</div>
) : (
<p className="text-xs text-stone-500 mb-3">Unable to load balance</p>
)}
<div className="flex gap-2">
{[5, 10, 25].map(amount => (
<button
key={amount}
onClick={() => handleTopUp(amount)}
disabled={isToppingUp}
className="flex-1 py-1.5 rounded-lg bg-primary-500/20 hover:bg-primary-500/30 text-primary-400 text-xs font-medium border border-primary-500/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
{isToppingUp ? '…' : `+$${amount}`}
</button>
))}
</div>
</div>
</div>
{/* ── Interval toggle ──────────────────────────────────── */}
+88 -1
View File
@@ -18,6 +18,7 @@ import {
type ModelInfo,
type Tool,
} from '../services/api/inferenceApi';
import { type TeamUsage, creditsApi } from '../services/api/creditsApi';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import type { NotionPageSummary, NotionSummary, NotionUserProfile } from '../store/notionSlice';
import {
@@ -141,6 +142,10 @@ const Conversations = () => {
const [isLoadingModels, setIsLoadingModels] = useState(false);
const [isSending, setIsSending] = useState(false);
const [sendError, setSendError] = useState<string | null>(null);
// Budget state
const [teamUsage, setTeamUsage] = useState<TeamUsage | null>(null);
const [isLoadingBudget, setIsLoadingBudget] = useState(false);
const isDragging = useRef(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const lastPanelWidthRef = useRef(panelWidth);
@@ -219,6 +224,18 @@ const Conversations = () => {
.finally(() => setIsLoadingModels(false));
}, []);
// Fetch inference budget on mount
useEffect(() => {
setIsLoadingBudget(true);
creditsApi
.getTeamUsage()
.then(data => setTeamUsage(data))
.catch(() => {
// Budget unavailable — silently ignore
})
.finally(() => setIsLoadingBudget(false));
}, []);
// Remove thread fetching - threads are now loaded from Redux persist
// Sync URL → Redux: when URL has a threadId param, select that thread
@@ -995,6 +1012,34 @@ const Conversations = () => {
{/* Message Input */}
<div className="flex-shrink-0 border-t border-white/10 px-4 py-3">
{/* Budget depleted banner — show top-up CTA */}
{teamUsage && teamUsage.remainingUsd <= 0 && (
<div className="mb-3 p-3 rounded-xl bg-coral-500/10 border border-coral-500/20 flex items-center justify-between gap-3">
<div className="flex items-center gap-2 min-w-0">
<svg
className="w-4 h-4 text-coral-400 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<p className="text-xs text-coral-300 truncate">
Daily inference budget exhausted. Top up to continue.
</p>
</div>
<button
onClick={() => navigate('/settings/billing')}
className="flex-shrink-0 px-3 py-1.5 rounded-lg bg-coral-500 hover:bg-coral-400 text-white text-xs font-medium transition-colors">
Top Up
</button>
</div>
)}
{/* Show warning if another thread is active */}
{activeThreadId && activeThreadId !== selectedThreadId && (
<div className="mb-3 p-2 rounded-lg bg-amber-500/10 border border-amber-500/20">
@@ -1004,7 +1049,7 @@ const Conversations = () => {
</p>
</div>
)}
{/* Model selector */}
{/* Model selector + budget indicator */}
<div className="flex items-center gap-2 mb-2">
{isLoadingModels ? (
<span className="text-xs text-stone-600">Loading models</span>
@@ -1030,6 +1075,48 @@ const Conversations = () => {
</select>
</>
)}
<div className="flex-1" />
{/* Budget indicator — circular */}
{(isLoadingBudget || teamUsage) && (() => {
const size = 22;
const r = 9;
const circ = 2 * Math.PI * r;
const pct = teamUsage
? Math.min(1, teamUsage.remainingUsd / teamUsage.cycleBudgetUsd)
: 0;
const dash = pct * circ;
return (
<div className="flex items-center gap-1.5" title={teamUsage ? `$${teamUsage.remainingUsd.toFixed(2)} of $${teamUsage.cycleBudgetUsd.toFixed(2)} remaining` : 'Loading budget…'}>
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className="-rotate-90">
<circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="currentColor" strokeWidth="2.5" className="text-white/10" />
{teamUsage ? (
<circle
cx={size / 2} cy={size / 2} r={r}
fill="none" stroke="currentColor" strokeWidth="2.5"
strokeDasharray={`${dash} ${circ}`}
strokeLinecap="round"
className={pct < 0.2 ? 'text-amber-500' : 'text-primary-500'}
style={{ transition: 'stroke-dasharray 0.3s ease' }}
/>
) : (
<circle
cx={size / 2} cy={size / 2} r={r}
fill="none" stroke="currentColor" strokeWidth="2.5"
strokeDasharray={`${circ * 0.25} ${circ}`}
strokeLinecap="round"
className="text-stone-600 animate-spin origin-center"
style={{ transformOrigin: `${size / 2}px ${size / 2}px` }}
/>
)}
</svg>
{teamUsage && (
<span className="text-[10px] text-stone-500">
${teamUsage.remainingUsd.toFixed(2)}
</span>
)}
</div>
);
})()}
</div>
{sendError && (
<div className="flex items-center justify-between mb-2">
+6 -12
View File
@@ -8,7 +8,6 @@ import { setOnboardedForUser } from '../../store/authSlice';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import FeaturesStep from './steps/FeaturesStep';
import GetStartedStep from './steps/GetStartedStep';
import InviteCodeStep from './steps/InviteCodeStep';
import PrivacyStep from './steps/PrivacyStep';
const Onboarding = () => {
@@ -16,14 +15,13 @@ const Onboarding = () => {
const dispatch = useAppDispatch();
const user = useAppSelector(state => state.user.user);
const [currentStep, setCurrentStep] = useState(0);
const totalSteps = 4;
const totalSteps = 3;
// Lottie animation files for each step
const stepAnimations = [
'/lottie/trophy.json', // Step 1 - Invite Code
'/lottie/wave.json', // Step 2 - Features
'/lottie/safe3.json', // Step 3 - Privacy
'/lottie/trophy.json', // Step 4 - Get Started
'/lottie/wave.json', // Step 1 - Features
'/lottie/safe3.json', // Step 2 - Privacy
'/lottie/trophy.json', // Step 3 - Get Started
];
const handleNext = () => {
@@ -54,15 +52,11 @@ const Onboarding = () => {
const renderStep = () => {
switch (currentStep) {
case 1:
return <InviteCodeStep onNext={handleNext} />;
case 2:
return <FeaturesStep onNext={handleNext} />;
case 3:
return <PrivacyStep onNext={handleNext} />;
case 4:
case 2:
return <GetStartedStep onComplete={handleComplete} />;
default:
return <InviteCodeStep onNext={handleNext} />;
return <FeaturesStep onNext={handleNext} />;
}
};
+53 -16
View File
@@ -1,25 +1,38 @@
import type { ApiResponse } from '../../types/api';
import { apiClient } from '../apiClient';
interface CreditBalance {
balance: number;
currency: string;
export interface CreditBalance {
balanceUsd: number;
topUpBalanceUsd: number;
}
interface CreditTransaction {
export interface TeamUsage {
remainingUsd: number;
cycleBudgetUsd: number;
dailyUsage: number;
totalInputTokensThisCycle: number;
totalOutputTokensThisCycle: number;
}
export interface TopUpResult {
url: string;
gatewayTransactionId: string;
amountUsd: number;
gateway: string;
}
export interface CreditTransaction {
id: string;
amount: number;
type: 'credit' | 'debit';
description: string;
type: 'EARN' | 'SPEND';
action: string;
amountUsd: number;
balanceAfterUsd: number;
createdAt: string;
// Add other fields based on backend schema
}
interface PaginatedTransactions {
export interface PaginatedTransactions {
transactions: CreditTransaction[];
totalCount: number;
page: number;
limit: number;
total: number;
}
/**
@@ -27,11 +40,35 @@ interface PaginatedTransactions {
*/
export const creditsApi = {
/**
* Get the current user's credit balance
* Get the current user's credit balance (general + top-up)
* GET /credits/balance
*/
getBalance: async (): Promise<CreditBalance> => {
const response = await apiClient.get<ApiResponse<CreditBalance>>('/credits/balance');
const response = await apiClient.get<ApiResponse<CreditBalance>>('/payments/credits/balance');
return response.data;
},
/**
* Get team inference budget usage for the current billing cycle
* GET /teams/me/usage
*/
getTeamUsage: async (): Promise<TeamUsage> => {
const response = await apiClient.get<ApiResponse<TeamUsage>>('/teams/me/usage');
return response.data;
},
/**
* Start a top-up (get Stripe or Coinbase payment URL)
* POST /credits/top-up
*/
topUp: async (
amountUsd: number,
gateway: 'stripe' | 'coinbase' = 'stripe'
): Promise<TopUpResult> => {
const response = await apiClient.post<ApiResponse<TopUpResult>>('/payments/credits/top-up', {
amountUsd,
gateway,
});
return response.data;
},
@@ -39,9 +76,9 @@ export const creditsApi = {
* Get paginated credit transaction history
* GET /credits/transactions
*/
getTransactions: async (page = 1, limit = 50): Promise<PaginatedTransactions> => {
getTransactions: async (limit = 20, offset = 0): Promise<PaginatedTransactions> => {
const response = await apiClient.get<ApiResponse<PaginatedTransactions>>(
`/credits/transactions?page=${page}&limit=${limit}`
`/credits/transactions?limit=${limit}&offset=${offset}`
);
return response.data;
},