# esx_addonaccount (/docs/addons/esx_addonaccount)

esx_addonaccount is a server-only library resource that stores money outside of the normal player accounts. Use it whenever money belongs to a job, a business or a property instead of a player's wallet, for example society funds used by [esx_society](/docs/addons/esx_society) and [esx_billing](/docs/addons/esx_billing).

## Overview [#overview]

| | |
| --- | --- |
| Resource | `esx_addonaccount` (version `1.1`) |
| Side | Server only |
| Dependencies | `esx_lib`, `es_extended`, `oxmysql` |
| SQL file | `esx_addonaccount.sql` |
| Exports | `GetAccount`, `GetSharedAccount`, `AddSharedAccount` |

There are two kinds of accounts, controlled by the `shared` column of the `addon_account` table:

- **Shared** (`shared = 1`): a single account without an owner. Typical for `society_<job>` accounts.
- **Personal** (`shared = 0`): one account per player identifier. The resource creates the row automatically when a player loads (`esx:playerLoaded`) and stores the loaded accounts on the player as `xPlayer.get('addonAccounts')`.

## Installation [#installation]

<Steps>
<Step>
### Import the SQL [#import-the-sql]

Run `esx_addonaccount.sql` against your ESX database. It creates `addon_account` (the definitions) and `addon_account_data` (the balances). Both tables must use InnoDB.
</Step>
<Step>
### Ensure the resource [#ensure-the-resource]

Start it after `es_extended` and before every resource that uses it:

```bash title="server.cfg"
ensure oxmysql
ensure esx_lib
ensure es_extended
ensure esx_addonaccount
ensure esx_society
ensure esx_billing
```
</Step>
<Step>
### Define your accounts [#define-your-accounts]

Insert one row per account. Shared accounts get their balance row on startup automatically.

```sql
INSERT INTO `addon_account` (`name`, `label`, `shared`) VALUES
  ('society_mechanic', 'Mechanic', 1),
  ('property_black_money', 'Black money (property)', 0);
```
</Step>
</Steps>

On startup the resource runs a schema migration. It adds a generated `owner_key` column and a unique key on `(account_name, owner_key)` if they are missing. The migration aborts (and disables all accounts) instead of changing data when it finds problems:

- `addon_account_data` is not InnoDB
- rows with `account_name IS NULL` or `owner = ''`
- duplicate rows for the same account and owner

## Configuration [#configuration]

esx_addonaccount has no `config.lua`. Everything is driven by the `addon_account` table.

| Column | Type | Description |
| --- | --- | --- |
| `name` | `VARCHAR(60)` | Unique account name, e.g. `society_police` |
| `label` | `VARCHAR(100)` | Display label (stored, not used by this resource) |
| `shared` | `INT` | `1` = shared account, `0` = one account per player |

## Server exports [#server-exports]

### GetSharedAccount [#getsharedaccount]

```lua
exports.esx_addonaccount:GetSharedAccount(name)
```

| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | Account name from `addon_account` |

Returns the [account object](#account-object) or `nil` if the account does not exist or the resource is not ready yet.

```lua title="server/main.lua"
local account = exports.esx_addonaccount:GetSharedAccount('society_mechanic')

if account and account.addMoney(2500) then
    print(('Mechanic society now has $%s'):format(account.money))
end
```

### GetAccount [#getaccount]

```lua
exports.esx_addonaccount:GetAccount(name, owner)
```

| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | Name of a non-shared account |
| `owner` | `string` | Player identifier (`xPlayer.identifier`) |

Returns the personal account object or `nil`. Accounts are only kept in memory for loaded players (offline accounts are evicted about once a minute after they are saved), so call this for online players.

```lua title="server/main.lua"
local xPlayer = ESX.GetPlayerFromId(source)
local stash = exports.esx_addonaccount:GetAccount('property_black_money', xPlayer.identifier)

if stash then
    stash.removeMoney(1000)
end
```

### AddSharedAccount [#addsharedaccount]

```lua
exports.esx_addonaccount:AddSharedAccount(society, amount)
```

| Name | Type | Description |
| --- | --- | --- |
| `society` | `table` | `{ name = string, label = string }` |
| `amount` | `number?` | Starting balance, defaults to `0` |

Creates a shared account at runtime (inserts into both tables) and returns it. If the account already exists, the existing object is returned and nothing is inserted. Returns `nil` on invalid input or a failed insert.

```lua title="server/main.lua"
exports.esx_addonaccount:AddSharedAccount({ name = 'society_bahama', label = 'Bahama Mamas' }, 10000)
```

## Account object [#account-object]

| Field / method | Description |
| --- | --- |
| `name` | Account name |
| `owner` | Identifier, or `nil` for shared accounts |
| `money` | Current balance |
| `addMoney(amount)` | Adds money. Returns `true` or `false` |
| `removeMoney(amount)` | Removes money. Returns `false` if the balance would drop below 0 |
| `setMoney(amount)` | Sets the balance. Returns `true` or `false` |
| `save(cb?)` | Forces a write. `cb(success)` runs once the value is persisted |
| `isDirty()` | `true` while a change has not been written yet |
| `withPersistence(action)` | Runs `action(money)` while holding the persistence lock. Return `success, delta` to apply a change atomically |

`amount` must be a whole number between `0` and `2147483647`. Floats, negative numbers and `NaN` are rejected and the method returns `false`. Every successful change is saved to the database right away (with automatic retry every 5 seconds on failure).

## Events [#events]

All events are local server events. Clients cannot trigger them.

| Event | Arguments | When |
| --- | --- | --- |
| `esx_addonaccount:getSharedAccount` | `name, cb` | Legacy getter. `cb(account)` |
| `esx_addonaccount:getAccount` | `name, owner, cb` | Legacy getter. `cb(account)` |
| `esx_addonaccount:addMoney` | `name, amount, owner` | Fired after `addMoney` succeeded |
| `esx_addonaccount:removeMoney` | `name, amount, owner` | Fired after `removeMoney` succeeded |
| `esx_addonaccount:setMoney` | `name, amount, owner` | Fired after `setMoney` succeeded |
| `esx_addonaccount:refreshAccounts` | none | Reloads account definitions from the database |

Listening for changes is useful for logging:

```lua title="server/logs.lua"
AddEventHandler('esx_addonaccount:removeMoney', function(name, amount, owner)
    if name:find('^society_') then
        print(('[audit] %s lost $%s'):format(name, amount))
    end
end)
```

The getter events still work, but the exports are simpler and avoid callback nesting:

```lua
TriggerEvent('esx_addonaccount:getSharedAccount', 'society_police', function(account)
    if account then account.addMoney(500) end
end)
```

## Integration tips [#integration-tips]

- Society accounts follow the `society_<job name>` pattern. [esx_society](/docs/addons/esx_society) and [esx_billing](/docs/addons/esx_billing) rely on that naming.
- Store item stashes in [esx_addoninventory](/docs/addons/esx_addoninventory) and generic key/value data in [esx_datastore](/docs/addons/esx_datastore).
- New rows in `addon_account` are picked up without a restart when ESX fires `esx:sqlCatalogReady` for that table, or when you trigger `esx_addonaccount:refreshAccounts` on the server.

## Common issues [#common-issues]

- **`addMoney` returns `false`**: you passed a float (for example a percentage cut like `price * 0.15`). Round first with `math.floor`.
- **Console says "Duplicate addon accounts" or "Invalid account_name/empty owner"**: the migration stopped on purpose. Clean up the listed rows in `addon_account_data` by hand, then restart the resource.
- **Account is `nil` right after a server start**: another resource asked before esx_addonaccount finished loading. Check for `nil` and retry, or make sure the dependent resource starts later.

## Related [#related]

- [esx_society](/docs/addons/esx_society)
- [esx_addoninventory](/docs/addons/esx_addoninventory)
- [esx_datastore](/docs/addons/esx_datastore)
- [Money and accounts guide](/docs/guides/money-and-accounts)
- [Database schema](/docs/guides/database-schema)
