VouchNet's public API and MCP are paid for with prepaid credits, similar to a prepaid API account. There is no subscription and no unmetered tier: a developer adds credits first, each metered call spends credits from that developer's balance, and calls are refused once the balance is too low.
Credits are an additional gate. They do not replace credential scopes, distributed rate limits, audit logging, PKCE and secret verification, or human approval for protected social actions.
All pricing lives in one file: apps/web/app/lib/api-credits-config.ts.
The settings UI reads the same module, so the page always shows what the server enforces.
- Rate: 1,000 credits per US dollar (one credit is a tenth of a cent).
- Top-ups: one-time Stripe Checkout payments of $5, $10, $25, or $50 (5,000, 10,000, 25,000, or 50,000 credits). Credits do not expire.
- Per-call costs:
| Operation key | Entry point | Credits |
|---|---|---|
oauth.token |
POST /api/oauth/token, POST /api/v1/oauth/token |
1 |
oauth.userinfo |
GET /api/oauth/userinfo |
1 |
v1.applicant_data |
GET /api/v1/oauth/applicant-data, GET /api/v1/oauth/candidate-profile |
2 |
mcp.tool_call |
Each MCP tool call (reserved; the MCP transport is not live yet, see below) | 1 |
To change a price, edit apiCallCosts or creditTopUpPackages in that file. Costs must be
positive integers; top-ups are integer cents.
The owner of the integration client (the member who registered it under Settings → Developer center → Integration clients) pays. The member who authorizes a hiring site is never charged.
When the owner's balance cannot cover a call, the endpoint performs nothing and returns:
HTTP/1.1 402 Payment Required
Cache-Control: no-store
Content-Type: application/json
{
"error": "INSUFFICIENT_CREDITS",
"error_description": "The developer account that owns this client has insufficient prepaid API credits. Add credits in VouchNet settings and retry.",
"required_credits": 2,
"balance_credits": 1,
"top_up_url": "https://vouchnet.dev/settings/developers"
}For token exchange, a 402 rolls back before the one-use authorization code is consumed, so the client can retry the same code (within its five-minute lifetime) after topping up.
No. Only successful calls are charged. The debit is written in the same database transaction as the work it pays for and commits only if that work succeeds. Calls rejected for an invalid or expired token, a missing scope, a revoked client, a bad client secret or PKCE verifier, a rate limit, insufficient credits, or a server error cost nothing. Credential checks run before the debit, and the token endpoint's existing distributed rate limit also bounds repeated failing requests.
- Ledger: migration
0029_api_credits.sqladdsapi_credit_balances(one integer balance per member,CHECK (balance_credits >= 0)) andapi_credit_ledger(append-only top-ups, usage, and adjustments with the balance after each entry). Amounts are integers; nothing is stored as a float. - Atomic deduction: a call spends credits with a single conditional statement
(
UPDATE … SET balance_credits = balance_credits - cost WHERE balance_credits >= cost) that also writes the usage ledger row. PostgreSQL locks the balance row and re-checks the condition against the latest committed value, so concurrent requests serialize and cannot overspend. - Top-ups:
POST /api/billing/credits/checkoutcreates a one-time Checkout Session with inline USDprice_data(no Stripe Product or Price needs to exist). The browser redirect grants nothing. The signedcheckout.session.completedwebhook (orcheckout.session.async_payment_succeededfor delayed payment methods) credits the balance only when Stripe reportspayment_status: paid. Credits are computed from the integeramount_totalStripe charged, not from metadata. The ledger stores the Checkout Session id under a unique constraint, so a redelivered or duplicated event can never credit twice. - Inventory guard:
app/lib/api-credit-entry-points.test.tsscans every route underapp/api/v1,app/api/oauth,app/api/mcp, andapp/mcp. A new route there fails the test until it is credit-gated or explicitly exempted with a reason.
GET /api/v1/status— unauthenticated health check.GET /api/v1/me— first-party read model that only accepts a VouchNet browser session cookie.GET /api/v1/resumes/download— signed single-resume capability link; not a client credential.POST /api/oauth/authorize— the member's own consent decision in the VouchNet UI./api/desktop/*— VouchNet's own first-party Desktop app acting for the signed-in member.
The MCP gateway transport is not live yet (see MCP); there is no MCP route to meter
today. When it ships, each tool call must spend mcp.tool_call credits through spendCredits
inside the tool's transaction (or chargeApiCall for non-transactional tools) and return the same
402 body. The inventory test enforces this for routes under app/api/mcp and app/mcp.
Signed-in members see their balance, 30-day usage, the most recent ledger entries, and add-credit
buttons under Settings → Account preferences and Settings → Developer center.
GET /api/billing/credits returns the same data for the signed-in member.
No new environment variables are required. Credit Checkout uses the existing
STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET, and the existing webhook endpoint
https://vouchnet.dev/api/billing/webhook, which is already subscribed to
checkout.session.completed. If delayed payment methods (for example, bank debits) are enabled in
Stripe Checkout, also subscribe the endpoint to checkout.session.async_payment_succeeded.