Ledger
The functions on the Ledger module itself.
Look up any function on Ledger itself.
local Name = Ledger.Id() -- a fresh name for Erase and Reset
if Name == nil then
return false -- Busy: try again later
end
local Ok, Result = Bank:Erase(Key, { Id = Name })Ledger runs on the server only. On a client, only Ledger.Reason works.
| Function | What it does |
|---|---|
Ledger.New<<Data, Ops>>(Config) | Makes a store. Make each store once, in a Stores module, and require it from every script. See Getting started. |
Ledger.Id() | Makes a name for Erase and Reset. |
Ledger.Now() | Ledger's clock in whole seconds. Use it for every IdAt. |
Ledger.Tx(Legs, Options?) | Changes several players or keys: all or none. |
Ledger.BeforeClose(Fn) | Runs Fn first when the server shuts down. |
Ledger.CloseAll() | Starts the shutdown now. Yields until it ends. |
Ledger.Reason | The names of all the answers, such as Ledger.Reason.Busy. |
New
Takes a config table and returns the store. Data is the shape of one key's data, and Ops is the type that lists every kind of op and what it carries. Sending an op that isn't in Ops is a type error.
| Setting | What it is |
|---|---|
Name | The store's name. Never change it once players have data. |
Keys | "Player" for player data, "String" for keys you choose, like a guild id. |
Default | What a new key starts with. Every field the data will ever have. |
MustExist | true: changing a key with no saved data answers Missing, except that a player's own Load starts them from Default. false: a key with no data starts from Default. |
Erasable | true: Erase works on this store. |
Reducer | Your function that applies an op. See Your data. |
Balances | Number fields Ledger changes for you. See below. |
Migrations | Changes to the data's shape. See Changing your data. |
Totals | Counters. See Global counters. |
Quantities | Limited stock. See Limited items. |
Schema | A function that checks every state the reducer returns. Return true, nil to keep it, or false and a message to refuse it. |
OnLoadFailed, Kick, KickMessage | What happens when a player's data won't load. Player stores only. See Players. |
Mock | A mock DataStore and MemoryStore to run on instead of the real ones, from the Mock package. One mock per server. See Shutdown and testing. |
If you write a Schema, give it a return type and always return both values, or Studio's type check will complain:
Schema = function(Data): (boolean, string?)
if Data.Gold < 0 then
return false, "Gold went below zero"
end
return true, nil
end,The defaults below are right for almost every game. Change them only if you know you need to.
| Setting | Default | What it is |
|---|---|---|
SaveInterval | 30 | Seconds between a session's saves, when it has changes waiting. |
IdleReadInterval | 120 | Seconds between a session's reads, when it has nothing waiting. This is how soon a player sees changes made from other servers. |
HoldMax | 900 | The longest a Quantity:Hold can last, in seconds. |
OrphanAge | 86,400 | Seconds before Ledger gives up on a trade whose deciding key never appeared, and hands its amounts back. |
CutWindow | 360 | Seconds a key remembers a Reset or Erase name, so a repeat of it is safe. |
Windows | 180 | Per op kind, { Timed = seconds, Untimed = seconds }: how long a key remembers a named op of that kind. See Once only. |
LegLimit | none | The most parts a Tx touching this store may have. |
Balances
Balances = {
Gold = {
Credit = { "AddGold", { Kind = "Refund", Max = NO_LIMIT } },
Debit = "SpendGold",
Max = 1000000,
},
},Each entry is a number field in Default. Ledger applies its ops itself, so your reducer never sees them.
| Option | What it is |
|---|---|
Credit | The op kinds that add Amount to the field: one name, or a list. |
Debit | The op kinds that take Amount away. |
Min | The field can't go below this. Default 0. |
Max | The field can't go above this. Default: no limit. |
Arithmetic | For amounts that aren't plain whole numbers, like very big numbers. Leave it out otherwise. |
A kind written as a table, { Kind = "Refund", Min = ..., Max = ... }, uses its own Min or Max in place of the field's. An op of a balance kind carries Amount, a whole number from 1 up. Each kind can only be named once, and not with one of Ledger's own names (Open, Take, Credit, Close, Hold, Confirm, Release, Bump, Cut, or one starting Debit:). See Your data.
Id and Now
Ledger.Id() answers a name, or nil, Busy if the server cannot make one yet (for example at startup in an outage). Ask again.
Make the name just before you use it. A server answers Expired for its own name once it is 6 minutes old.
Ledger.Now() counts seconds since 1 January 2026. Take it once per op, and use the same value on every retry. See Making a change happen once.
Tx
local Legs = {
PlayerStore:Leg(From, { Kind = "SpendGold", Amount = Amount }),
PlayerStore:Leg(To, { Kind = "AddGold", Amount = Amount }),
}
-- Make the name once. Send the same Options again after Busy or Unresolved.
local Options: Ledger.TxOptions = { Id = "pay:" .. HttpService:GenerateGUID(false), IdAt = Ledger.Now() }
local Ok, Why = Ledger.Tx(Legs, Options)Each player or key in the trade is one entry, { Store, Key, Op, MustExist? }. Store:Leg builds one with the op type-checked. Use at least 2 entries, on different keys. The entries can be in different stores, such as a player store and a guild store. Options is { Id, IdAt }, both or neither. Id is a string you make, never a Ledger.Id() name.
| Answer | Means | Do |
|---|---|---|
true | Every change happened, once. | Done. |
Refused or Full | A rule said no. Nothing changed. Info.Key names the key. | Tell the player. Do not resend. |
Spent | The name was used with other terms, or the transaction was cancelled. Nothing changed. | Use a new name. |
Missing | A key that must exist is not there. | Check Info.Key. |
Behind | A newer build owns a key. | Stop writing it from this build. |
Busy or NoRoom | Nothing was sent. | Send the same name again later. |
Unresolved | Not known yet. All the changes or none will happen. | Send the same name, IdAt and list again. Info.Outcome tells you later. |
BeforeClose and CloseAll
Ledger.BeforeClose(function()
-- runs first when the server shuts down, for up to 5 seconds
end)Roblox already starts the shutdown for you. Call CloseAll only to end it sooner. BeforeClose throws if the server is already closing. See Shutdown and testing.
Reason
Ledger.Reason.Refused == "Refused". Use it to avoid mistyping an answer. There are 15 answers; see Answers.
Next: Store