ceejay.economy
Read, grant and deduct a member's currency.
Every function takes the same last argument, options: signal, timeoutMs and
maxRetries, for that one call. A write also takes idempotencyKey there. Every function
can also raise the errors any request can raise, such as a bad key or the rate limit. Both
are on Errors and retries.
Idempotency keys
The two writes on this page take one more option, idempotencyKey, as every write does.
It is text you choose, 1 to 128 characters, for one request.
- When the same request is sent again with its key, no currency is moved a second time. The
response is the original one, with
duplicate: true. - With a key, the SDK retries the request automatically after a
429, a5xxor no response. Without a key it never retries, because CEEJAY would move the currency twice. - Use a new key for every separate request, and the same key only to retry one. A request
counts as the same when the member and the amount match; the note may differ. The same key with
another member or another amount is rejected with
idempotency_key_reused. - Each key identifies one request. When one match grants coins and XP, send two keys, such as
match-9:coinsandmatch-9:xp. - A key belongs to the API key that sent it.
Generate the key from something that identifies the payment in your own system, such as an order ID. A key generated from the time of the call is new on every attempt, and protects nothing.
A key on these two functions keeps working on an instance that is still being updated, and a key that has moved currency once is always recognised as a repeat.
ceejay.economy.getBalance
Gets a member's balance and lifetime totals in your community's currency.
GET /v1/economy/balance · requires economy:read · API reference: Get currency balance
Retried by the SDK: Yes. It is a read
Takes
Prop
Type
Returns Schemas['EconomyBalanceResult'].
Prop
Type
Errors
| Status | code | When |
|---|---|---|
404 | not_available | This instance has no currency yet |
A member with no balance yet is not an error: every number is 0. To tell "no account"
from "zero balance", call ceejay.members.get, which returns 404 for a member
CEEJAY has never seen.
Example
const wallet = await ceejay.economy.getBalance('409876543210987654')
console.log(wallet.balance, wallet.totalEarned, wallet.totalSpent)ceejay.economy.grant
Adds currency to a member's balance. The member's wallet is created if they have none.
POST /v1/economy/grant · requires economy:grant · API reference: Grant currency
Retried by the SDK: Only with idempotencyKey
Takes one object, then options.
Prop
Type
Options
Prop
Type
The API also accepts the note under its alias description, and the key as
idempotency_key, from code that calls it without the SDK. The SDK sends the names in the
tables.
Returns Schemas['GrantCurrencyResult'].
Prop
Type
The response also carries the note under its alias description, which is
still returned.
A balance cannot exceed the community's maximum, which is 1,000,000 unless the community set
a different one. Reaching it is not an error: read capped and amountGranted. The grant works
whether or not the community's economy is switched on.
Errors
| Status | code | When |
|---|---|---|
400 | bad_request | A field is missing or not valid, amount is above 1,000,000, or the note is not a string |
404 | member_not_found | CEEJAY has no profile for this ID, and Discord says it is not in your server. A member who has only just joined can take up to 15 seconds to be found: wait 15 seconds before you retry |
404 | not_available | This instance has no currency yet |
409 | balance_limit_reached | The grant would take the member's lifetime total past the largest number CEEJAY can store. Nothing moved |
503 | discord_unavailable | CEEJAY has no profile for this ID and could not query Discord about it. Nothing was moved. The SDK retries when you sent a key |
Errors about an idempotency key are described on Errors and retries.
Example
const grant = await ceejay.economy.grant(
{ discordId: '409876543210987654', amount: 500, reason: 'Weekly tournament prize' },
{ idempotencyKey: 'tournament-2026-40-winner' },
)
if (grant.duplicate) {
console.log('Already paid. Nothing moved this time.')
} else if (grant.capped) {
console.log(`Only ${grant.amountGranted} of ${grant.amountRequested} fitted under the maximum`)
} else {
console.log(`Paid. The balance is now ${grant.balance}`)
}ceejay.economy.deduct
Deducts currency from a member. The balance never goes below zero: when the member has
less than amount, nothing is deducted and the request is rejected.
POST /v1/economy/deduct · requires economy:deduct · API reference: Deduct currency
Retried by the SDK: Only with idempotencyKey
Takes one object, then options.
Prop
Type
Options
Prop
Type
The API also accepts the note under its alias description, and the key as
idempotency_key, from code that calls it without the SDK. The SDK sends the names in the
tables.
Returns Schemas['DeductCurrencyResult'].
Prop
Type
The response also carries the note under its alias description, which is
still returned.
A deduction is not counted as the member's spending: totalSpent does not change. The
deduction works whether or not the community's economy is switched on.
Errors
| Status | code | When |
|---|---|---|
400 | bad_request | A field is missing or not valid, or the note is not a string |
400 | insufficient_balance | The member has less than amount. Nothing was deducted. error.body contains balance, the member's current balance, and requested. currentBalance is an alias of balance and is still returned |
404 | not_available | This instance has no currency yet |
insufficient_balance returns status 400 for backward compatibility, although other rule-based
errors return 409. Check the code.
Errors about an idempotency key are described on Errors and retries.
Example
import { CeejayError } from '@communitystudios/ceejay'
try {
const deduction = await ceejay.economy.deduct(
{ discordId: '409876543210987654', amount: 300, reason: 'Entry fee, cup 12' },
{ idempotencyKey: 'cup-12-entry-409876543210987654' },
)
console.log(deduction.amountDeducted, deduction.balance)
} catch (error) {
if (error instanceof CeejayError && error.code === 'insufficient_balance') {
const refusal = error.body as { balance: number; requested: number }
console.log(`The member holds ${refusal.balance}, which is less than the fee of ${refusal.requested}`)
} else {
throw error
}
}
