CEEJAYDocs

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, a 5xx or 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:coins and match-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

StatuscodeWhen
404not_availableThis 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

ceejay.economy.getBalance
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

StatuscodeWhen
400bad_requestA field is missing or not valid, amount is above 1,000,000, or the note is not a string
404member_not_foundCEEJAY 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
404not_availableThis instance has no currency yet
409balance_limit_reachedThe grant would take the member's lifetime total past the largest number CEEJAY can store. Nothing moved
503discord_unavailableCEEJAY 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

ceejay.economy.grant
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

StatuscodeWhen
400bad_requestA field is missing or not valid, or the note is not a string
400insufficient_balanceThe 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
404not_availableThis 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

ceejay.economy.deduct
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
  }
}

On this page

Community StudiosPrivacyTerms