Ledger

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.

FunctionWhat 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.ReasonThe 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.

SettingWhat it is
NameThe store's name. Never change it once players have data.
Keys"Player" for player data, "String" for keys you choose, like a guild id.
DefaultWhat a new key starts with. Every field the data will ever have.
MustExisttrue: 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.
Erasabletrue: Erase works on this store.
ReducerYour function that applies an op. See Your data.
BalancesNumber fields Ledger changes for you. See below.
MigrationsChanges to the data's shape. See Changing your data.
TotalsCounters. See Global counters.
QuantitiesLimited stock. See Limited items.
SchemaA function that checks every state the reducer returns. Return true, nil to keep it, or false and a message to refuse it.
OnLoadFailed, Kick, KickMessageWhat happens when a player's data won't load. Player stores only. See Players.
MockA 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.

SettingDefaultWhat it is
SaveInterval30Seconds between a session's saves, when it has changes waiting.
IdleReadInterval120Seconds between a session's reads, when it has nothing waiting. This is how soon a player sees changes made from other servers.
HoldMax900The longest a Quantity:Hold can last, in seconds.
OrphanAge86,400Seconds before Ledger gives up on a trade whose deciding key never appeared, and hands its amounts back.
CutWindow360Seconds a key remembers a Reset or Erase name, so a repeat of it is safe.
Windows180Per op kind, { Timed = seconds, Untimed = seconds }: how long a key remembers a named op of that kind. See Once only.
LegLimitnoneThe 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.

OptionWhat it is
CreditThe op kinds that add Amount to the field: one name, or a list.
DebitThe op kinds that take Amount away.
MinThe field can't go below this. Default 0.
MaxThe field can't go above this. Default: no limit.
ArithmeticFor 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.

AnswerMeansDo
trueEvery change happened, once.Done.
Refused or FullA rule said no. Nothing changed. Info.Key names the key.Tell the player. Do not resend.
SpentThe name was used with other terms, or the transaction was cancelled. Nothing changed.Use a new name.
MissingA key that must exist is not there.Check Info.Key.
BehindA newer build owns a key.Stop writing it from this build.
Busy or NoRoomNothing was sent.Send the same name again later.
UnresolvedNot known yet. All the changes or none will happen.Send the same name, IdAt and list again. Info.Outcome tells you later.

More in Answers and Trading.

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

On this page