Simulator Finance logo

Simulator Finance

Organization
corezoid
simulator-finance

Simulator.Company financial management specialist. Use when the user wants to manage financial and metric accounts on actors (balances, counters, plan vs fact), record transactions, transfer value between accounts, work with currencies and account-name categories, track non-financial metrics, read balances and turnover, tag accounts for grouping, or set up account triggers (balance / transaction-count / data-field alerts). Activate when the user mentions "record transaction", "transfer funds", "account balance", "financial tracking", "depreciation", "expense", "budget", "counter", "mileage tracking", "account tag", "account trigger", "balance alert", "запиши транзакцію", "переказ коштів", "баланс рахунку", "лічильник", "пробіг", "тег рахунку", "тригер на рахунок", "запиши транзакцию", "перевод средств", "баланс счёта", "счётчик", "пробег", "тег счёта", "триггер на счёт". Accounts attach to ACTORS — use `simulator-actors` to create/find the actor first; for sharing an account use `simulator-access`; for dashboards use `simulator-charts`.

Overview

Publishercorezoid
Repositorysimulator-ai-plugin
Skill namesimulator-finance
Stars
61
Forks
13
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by corezoid on GitHub. Read the source before you install it.

Installation

Install the Simulator Finance AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/corezoid/simulator-ai-plugin/tree/main/plugins/simulator/skills/simulator-finance
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/corezoid/simulator-ai-plugin.git /tmp/simulator-ai-plugin
mkdir -p .claude/skills
cp -r /tmp/simulator-ai-plugin/plugins/simulator/skills/simulator-finance .claude/skills/simulator-finance
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Simulator Finance in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Simulator Finance on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Simulator Finance is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Curated tool names (v2 server) — call these EXACT names.

OperationTools
Pair bootstrap (required first)createAccountPair (creates name + currency if missing AND self-grants access — the only way to avoid 403s on a non-owner workspace)
AccountscreateAccount getAccount getAccounts getBalance getChildAccounts updateAccount setAccountAmount deleteAccount setAccountFormula getAccountFormula
Tags & triggerssaveAccountActors (link tag/trigger actors to a pair or one account) · getDataFieldActorsByActor saveDataFieldActorsByActor getDataFieldActorsByForm saveDataFieldActorsByForm (data triggers) · read via getAccounts(withTags/withTriggers)
CurrenciescreateCurrency getCurrencies searchCurrencies
Account namescreateAccountName getAccountNames updateAccountName searchAccountNames
CounterssaveCounters setCounters getCounters
TransactionscreateTransaction finalizeTransaction atomCreateTransaction getTransactions getAccountTransactions getTransactionByRef
TransferscreateTransfer createTransferTwoStep getTransfer getTransferByRef filterTransfers

Tools take typed named arguments (not a JSON body string). accId defaults to the configured workspace if omitted.

MANDATORY pairing rule: every flow that ends up creating an account or a currency (or a new account-name) MUST go through createAccountPair first. It both ensures the (name, currency) pair exists and grants the caller pair-level access — without it, the very next getBalance / createTransaction / transfer call returns 403 Access Denied on any non-owner workspace. Prefer createAccountPair over a bare createCurrency + createAccountName pair, and always call it before createAccount for that (name, currency).

For user-to-user money movement you also need searchUsers / getUsers (find the userId) and getSystemActor (the user's twin actor) — see "Transfers between users" below.

Simulator.Company Financial Manager

You manage the financial subsystem via the simulator MCP server. Accounts live on actors — each actor can have many accounts, one per (accountName, currency, accountType).

Actor
  └── Account  (nameId + currencyId + accountType)
        ├── Transactions  (debits/credits on ONE account, with history)
        └── Transfers      (atomic debit-leg(s) + credit-leg(s) across accounts)
Counters     (Scylla-backed tallies — accounts WITHOUT transaction history; see below)

Workspace Context Check (MANDATORY FIRST STEP)

Before any tool call, verify the Workspace ID (accId) is known. If the user has not provided it (in the message, history, or session):

Ask the user — in their own language (English, Ukrainian, or Russian) — which workspace to work in, i.e. for the Workspace ID (accId).

Do not call workspace-scoped tools until accId is known (it then defaults for the rest).

Relationship to the other skills

  • simulator-actors — create/find the actor an account hangs off (you need its actorId).
  • simulator-access — share an account / grant who may view or modify it.
  • simulator-charts — visualise balances/turnover on a layer.

Core concepts

Amounts are REAL decimal values — never minor units

amount: 500 on a USD account means 500 USD. A currency's precision (a.k.a. decimals) is display only: precision: 2 renders 1600 as 1600.00 — it does not mean stored 1600 is 16.00. When you write or read an amount, use the actual value; never multiply/divide by 10^precision. (highPrecision=true on reads returns extra fractional digits.)

What types an account: accountType + counterType (NOT asset/liability)

There is no asset/liability/expense/income enum on an account. What an account means (cash, maintenance, revenue, …) is carried by its account name (nameId). An account is typed by two orthogonal dimensions:

accountType — the value type. Default fact.

accountTypeMeaning
factThe actual recorded balance (the normal case — usually omit, defaults to fact)
planA planned / budgeted figure (track plan vs fact)
min / max / avgAggregates over child actors' values

counterType — whether it is a normal balance or a fast counter. Default amount.

counterTypeMeaning
amountNormal balance account, full transaction history
counter / uniqCounterScylla-backed tally, NO history (analytics/anti-fraud); see Counters
systemCounterSystem-managed counter

So a normal account is accountType=fact, counterType=amount (both defaults — you can omit both). A mileage counter is counterType=counter.

incomeType (which direction raises the balance)

credit = credits increase the balance (money-in accounts); debit = debits increase it (cost accounts). It is a read / counter dimension (getAccounts, getTransactions, saveCounters filters), not a createAccount argument.

Prerequisites for an account

A currency (currencyId) and an account name (nameId) must exist in the workspace first, AND the caller must have access to the (nameId, currencyId) pair. Use createAccountPair to do both at once — it creates the account name and the currency if they are missing, and self-grants the caller view+modify+remove on the pair. Do not call a bare createCurrency + createAccountName and skip createAccountPair: the pair would still have no access rules, so the next getBalance/createTransaction/transfer 403s on any non-owner workspace.

Why the pair matters. The (nameId, currencyId) pair — written <nameId>_<currencyId> — is its own access-controlled object (objType="account"). createAccount only checks access to the actor, so it happily attaches the account, but it does not grant you access to the pair. Every per-account operation afterwards (getBalance, getAccount, setAccountAmount, createTransaction, transfers) re-checks pair access and will return 403 Access Denied if you have none — unless you are the workspace Owner, who bypasses the check. See Account-pair access below.


Currencies

getCurrencies(accId="ws_xxx", filter="id,name,symbol")
searchCurrencies(accId="ws_xxx", query="USD")          # find one by name

createCurrency(accId="ws_xxx", name="USD", symbol="$", precision=2)
# Non-financial units for counters:
createCurrency(accId="ws_xxx", name="Km",    symbol="km", precision=0)
createCurrency(accId="ws_xxx", name="Units", symbol="u",  precision=0)

Args are name / symbol / precision (not title / decimals).

Prefer createAccountPair over a bare createCurrency when you are creating a currency in order to use it on an account. createAccountPair(accId, accountName, currencyName, symbol?, precision?, type?) creates the currency (and the account name) if missing AND self-grants pair access in one call — createCurrency on its own leaves the pair without an access rule. Only call the bare createCurrency when you genuinely need a workspace currency that no (name, currency) pair will use (rare).

Account names (categories)

getAccountNames(accId="ws_xxx", filter="id,name")
searchAccountNames(accId="ws_xxx", query="maint")

createAccountName(accId="ws_xxx", name="Maintenance", abbreviation="MNT")
updateAccountName(nameId="<account-name id>", name="Maintenance costs")

# Make a category transfer-only: every account using this name then rejects plain
# transactions and can only be moved via a transfer (see "Transfer-only categories" below).
updateAccountName(nameId="<account-name id>", name="Settlement", transferOnly=true)
updateAccountName(nameId="<account-name id>", name="Settlement", transferOnly=false)  # lift it

Same rule as currencies: prefer createAccountPair when you are creating an account name that will be used on an account — it creates the name (and currency) if missing AND grants pair access. Reserve bare createAccountName for rare workspace-level housekeeping that does not lead to an account.

Transfer-only categories

An account name can be flagged transferOnly (boolean, default false). When it is true, every account that uses that account name rejects plain balance changes — single transactions, atomic multi-account transactions, and standalone 2-step authorize/complete all fail with HTTP 400 ("…is transfer-only; use a transfer instead of a plain transaction"). Only a transfer (createTransfer) moves such balances, because each transfer leg is tied to a transfer record. Internal limit changes and the cancel/reverse of an existing transfer are not affected. Use it for clearing/settlement categories that must always balance against a counterparty. Set/clear it with updateAccountName(..., transferOnly=...); omit the field to leave it unchanged. The flag is returned on every account-name read.


Accounts (on an actor)

# MANDATORY first: bootstrap the (name, currency) pair AND grant yourself access.
# This creates the account-name + currency if missing. Always run it for the (name, currency)
# you are about to use, BEFORE createAccount. Idempotent / safe to repeat.
createAccountPair(accId="ws_xxx", accountName="Maintenance", currencyName="USD",
                  symbol="$", precision=2)

# Attach an account to an actor (the pair must already exist + you must have pair access).
# accountType defaults to fact and counterType to amount — omit both for a normal account.
createAccount(actorId="<actor UUID>", nameId="<account-name id>", currencyId=1,
              treeCalculation=false,         # aggregate child-actor balances into this one
              minLimit=0, maxLimit=null,     # optional balance limits
              ignoreIfExist=true)            # don't error if it already exists

# A counter account (fast Scylla tally, no history): set counterType
createAccount(actorId="<actor UUID>", nameId="<mileage name>", currencyId=2, counterType="counter")

# Read
getAccounts(actorId="<actor UUID>", filter="id,amount,currencyId,nameId")   # all on the actor
getAccount(accountId="<account id>", trsCount=true)                          # one by id
getBalance(actorId="<actor UUID>", currencyId=1, nameId="<account-name id>") # one by coordinates
getChildAccounts(actorId="<actor UUID>", nameId="<account-name id>", currencyId="1") # same account on child actors

# Settings / fixed balance (accountType is part of the account key — usually "fact")
updateAccount(actorId="<actor UUID>", currencyId=1, nameId="<aname>", accountType="fact",
              treeCalculation=true, minLimit=0)
setAccountAmount(accountId="<account id>", amount=25000)   # set a FIXED balance (correction) — confirm first

# Delete (irreversible — confirm first)
deleteAccount(actorId="<actor UUID>", currencyId=1, nameId="<aname>", accountType="fact")
  • setAccountAmount overrides the balance to a fixed value; createTransaction changes it incrementally with history. Prefer transactions for anything auditable.
  • getAccounts(from, to) (unixtime milliseconds) returns each account's turnover for that window instead of the live balance.

Locating which account to use — resolve the account NAME first, then the currency. An actor usually holds several accounts; the nameId (account name) is the primary dimension — it carries the meaning (Wallet, Maintenance, Trust, …) — and currencyId only narrows it. So when the user names an amount ("100 trusts from Lugovoy"): first resolve the account name (searchAccountNames/getAccountNames → nameId), then the currency (searchCurrencies/getCurrencies → currencyId), then pick the account on the actor with getAccounts / getBalance(actorId, nameId, currencyId). Don't match on currency alone — two different account names can share one currency. (If the named unit IS the account name and the currency is implicit, resolve the name and let getAccounts reveal the currency.)

Multiple accounts on the same (nameId, currencyId) — offer Debit / Credit / Total. Simulator accounts are directional ledger entries: an actor can hold two rows for the same (nameId, currencyId) pair — one incomeType="debit", one incomeType="credit" — instead of one netted balance. If getAccounts returns more than one account for the pair you resolved, don't ask the user to choose only between "Debit" and "Credit": also offer Total (the net balance, credit.amount - debit.amount) as a third option. Ask which the user wants before proceeding.

Formula (computed) accounts

Make an account's balance a computed expression over other accounts with setAccountFormula (and inspect it with getAccountFormula):

setAccountFormula(accountId="<calcAccountId>",
  formula="<srcAccUuidA> + <srcAccUuidB> * 0.2")   # numeric mathjs expression
getAccountFormula(accountId="<calcAccountId>")       # the source accounts it references (or {})
  • Reference source accounts by their account UUID (the account id from getAccount/getAccounts — NOT the actor id). Each UUID resolves to that account's available balance (amount − hold), then the expression is evaluated (mathjs: + - * / ( ), functions). The numeric result is the account's balance and recomputes automatically when a source changes.
  • You cannot set a formula on an account that already has transactions — the backend rejects it. Use a fresh/formula-only account.
  • Clear the formula (back to a plain account) by passing an empty string for formula.
  • See accounts.md → "Formula-Based Accounts". (Block/aggregate-by-config accounts beyond this remain UI-only — don't fabricate a tool call for those.)

Account tags & triggers (actors linked to accounts)

Both features ride one mechanism: an ordinary actor of a system form is LINKED to an account. So tag/trigger CRUD is actor CRUD (createActor / updateActor / deleteActor — simulator-graph tools) plus the link tools below; deleting the actor removes the tag/trigger everywhere.

FeatureSystem formLinked toEffect
TagTagsthe (nameId, currencyId) pairlabels/groups every account of the pair
Balance / count triggerAccountTriggersthe pair, or ONE account (accountId)evaluated on every transaction of the linked account(s)
Data triggerAccountTriggers (valueType=data)an actor's or a form's data fieldevaluated when the watched field changes

Find the two system form ids once per workspace:

getForms(accId="ws", formTypes="system", filter="id,title")   # → Tags id, AccountTriggers id

Tags

# 1. A tag is an actor on the Tags system form
createActor(formId=<Tags form id>, title="VIP")                  # → tag actor UUID

# 2. Link tags to the account PAIR — REPLACE semantics; ALWAYS scope with formId
saveAccountActors(nameId="<aname>", currencyId=1,
                  formId=<Tags form id>,                         # replace tags only
                  actors=["<tag1 UUID>", "<tag2 UUID>"])

# 3. Read / filter
getAccounts(actorId="<actor>", withTags=true)            # each account + its tags
getAccounts(actorId="<actor>", tag="<tag actor UUID>")   # only accounts carrying that tag
getAccounts(actorId="<actor>", ungrouped=true)           # only accounts with NO tags

# 4. Untag / rename / delete the tag itself
saveAccountActors(nameId="<aname>", currencyId=1, formId=<Tags form id>, actors=[])
updateActor(...) / deleteActor(...)                      # the tag actor itself
  • Tags sit on the workspace-level pair, so the tag shows on that (name, currency) account of every actor — they are not per-actor labels.
  • saveAccountActors REPLACES the linked set. To add a tag: read the current set (getAccounts(withTags=true)) and resend it plus the new UUID. Never omit formId on a pair-level call — an unscoped replace also wipes the pair's triggers.

Balance & transaction-count triggers

A trigger is an actor on the AccountTriggers system form. Its data fields (select / multiSelect values use the [{"title": "...", "value": "..."}] shape):

data fieldValuesMeaning
valueType (required)amount | count | datawhat to watch: account balance, number of transactions, or an actor data field
accountIncomeTypemulti: total | debit | creditwhich balance(s) to watch (default Total)
lowerValue / upperValuefloatthe target-zone boundaries
periodName + periodValueday/month/quarter/year + intoptional calendar window — watch the period turnover instead of the lifetime balance
endDate{"endDate": <unixtime>}the trigger stops firing after this moment
operator + comparisonValue= | != + valuedata triggers only: comparison against the watched field
customParamsJSON stringpassed through to the webhook payload
# 1. Create the trigger actor
createActor(formId=<AccountTriggers form id>, title="Low balance alarm", data={
  "valueType": [{"title": "Account amount", "value": "amount"}],
  "accountIncomeType": [{"title": "Total", "value": "total"}],
  "lowerValue": 100, "upperValue": 100000
})

# 2. Link it — pair-wide, or to ONE account via accountId
saveAccountActors(nameId="<aname>", currencyId=1, formId=<AccountTriggers form id>,
                  actors=["<trigger UUID>"])                            # all accounts of the pair
saveAccountActors(nameId="<aname>", currencyId=1, formId=<AccountTriggers form id>,
                  accountId="<account id>", actors=["<trigger UUID>"])  # just this account

# 3. Inspect
getAccounts(actorId="<actor>", withTriggers=true)

Firing semantics. On each transaction of a linked account the watched value (balance or count, optionally over the period window) is classified into lowerZone (< lowerValue), upperZone (> upperValue) or normalZone — and the trigger fires only when the zone CHANGES since its previous call (no re-fire while the value stays in a zone). The event goes out as a type: "trigger" webhook to the API users that have access to the trigger actor (share it via simulator-access); customParams rides along in the payload.

Data triggers (watch an actor data field)

The same AccountTriggers actor with valueType=data + operator + comparisonValue, bound not to an account but to a data field — of one actor, or of every actor of a form:

saveDataFieldActorsByActor(actorId="<watched actor>", dataField="status",
                           actors=["<trigger UUID>"])          # one actor's field
saveDataFieldActorsByForm(formId=<watched form id>, dataField="status",
                          actors=["<trigger UUID>"])           # the field on ALL the form's actors

getDataFieldActorsByActor(actorId="<watched actor>", dataField="status")  # read back
getDataFieldActorsByForm(formId=<watched form id>, dataField="status")

Fires when the watched field changes and the =/!= outcome flips (matched ↔ unmatched) — not on every write. Both save tools REPLACE the bound set; actors=[] unbinds all.

dataField is the field id from the watched form, not its title — on user-created forms ids look like item_<digits> (semantic ids such as status exist only on system forms). Read the form (getForm) to find the right id — see simulator-actors.


Account-pair access (the usual cause of 403 Access Denied)

Account-level access is not granted per actor — it is granted on the workspace-level pair <nameId>_<currencyId> (objType="account"). The server resolves the pair's workspace via its account-name and then allows the call only if the caller is the workspace Owner, the pair is public+view, or the caller has an explicit access rule on that pair. createAccount is gated on actor access only and never seeds a pair access rule, so a non-owner (Admin / Member / Guest) who attaches an account still gets 403 on getBalance, getAccount, setAccountAmount, createTransaction and transfers. (Workspace owners don't see this because the owner shortcut grants view/modify on everything. System account-names are also exempt for view.)

Bootstrap your own access to a fresh pair — createAccountPair. This is the only call that self-grants access to a pair: it creates the account name + currency if missing and grants the caller view+modify+remove, so it is what makes a non-owner's accounts usable. It mirrors the UI modal CreateActorAccount (check pair → create pair → manage access → attach to actor). Run it once per (name, currency) — before or after createAccount, but before any balance/transaction read or write:

# Bootstrap the pair + your access (idempotent). BY NAME, not ids.
createAccountPair(accId="ws_xxx", accountName="Deal Value", currencyName="USD")

# Then attach to the actor and record value as usual.
createAccount(actorId="<actor UUID>", nameId="<Deal Value id>", currencyId=933)
createTransaction(accountId="<debit|credit account id>", amount=100, comment="…")
# → getBalance / getAccount on that pair now work too.

If the pair already has access rules and you are not among them, it returns 403 — then a workspace Owner (or an existing grantee) must grant you. Workspace Owners skip all of this (the owner shortcut covers them). To manage who else may use a pair you already hold:

# Grant a user/group access to ONE pair. objId = "<nameId>_<currencyId>" (composite, NOT the per-row account UUID).
saveAccessRules(
  objType="account",
  objId="<nameId>_<currencyId>",
  rules=[{ "action": "create",
           "data": { "saId": <saId>,                 # or userId / groupId
                     "privs": { "view": true, "modify": true, "remove": true } } }])

# Grant a WHOLE account category (all currencies of a name) across every actor.
# objId is the account-NAME id prefix — it matches all accounts named "<nameId>_*".
bulkSaveAccountPairsAccessRules(
  accId="ws_xxx",
  items=[{ "objId": "<nameId>",
           "rules": [{ "action": "create",
                       "data": { "saId": <saId>, "privs": { "view": true, "modify": true } } }] }])

# Inspect who currently has pair access (use the composite pair id, NOT the per-row account UUID):
getAccessRules(objType="account", objId="<nameId>_<currencyId>")

saveAccessRules / bulkSaveAccountPairsAccessRules require you to already hold the pair (or be the workspace Owner) — they do not self-grant. For your own first access to a brand new pair, createAccountPair is the only bootstrap.

Notes & gotchas:

  • The pair id is <nameId>_<currencyId> (e.g. c16d1bd0-…-…_933), not the per-row account UUID that createAccount returns. Using the bare UUID with the access-rule tools also 403s.
  • Granting pair access via the rule tools needs access to grant: only the workspace Owner (or someone who already holds the pair) can seed the first rule that way. For a brand-new pair use createAccountPair, which self-grants. If a pair already has rules that exclude you, an Owner must grant you (or make you workspace Owner, which resolves it globally).
  • This pair model is why account ops can 403 on a fresh local workspace where the user is only a Member/Guest, while the same flow works on cloud where the user is the workspace Owner.

Transactions (one account, with history)

# Immediate. `ref` makes it idempotent (safe to retry). Note: `comment`, not `description`.
createTransaction(accountId="<account id>", amount=450, comment="Oil change",
                  ref="service-jan-2024", data={"invoice": "INV-001"})

# 2-step hold → finalize. Step 1 authorizes; pass a `ref` so you can finalize by it.
createTransaction(accountId="<account id>", amount=500, comment="Pending maintenance",
                  ref="maint-hold-1", expiration=1735689600)   # creates an 'authorized' hold
finalizeTransaction(accountId="<account id>", type="completed", ref="maint-hold-1")  # or type="canceled"

# Atomic multi-leg (all-or-nothing) — e.g. double-entry depreciation
atomCreateTransaction(accId="ws_xxx", items=[
  { "accountId": "<asset acc>", "amount": -3000, "comment": "Depreciation debit", "ref": "dep-2023-a" },
  { "accountId": "<depr acc>",  "amount":  3000, "comment": "Depreciation credit", "ref": "dep-2023-b" }
])

# Read
getTransactions(actorId="<actor UUID>", currencyId=1, nameId="<aname>", limit=20)  # currencyId+nameId required
getAccountTransactions(accountId="<account id>", limit=20, offset=0)               # limit+offset BOTH required
getTransactionByRef(accountId="<account id>", ref="service-jan-2024")             # confirm a write landed

finalizeTransaction type ∈ completed | canceled (also declined/reversed); identify the hold by its ref (or parentRef), not a transaction id.

If a createTransaction/atomCreateTransaction here returns HTTP 400 "…is transfer-only", the account's account-name is flagged transferOnly — use createTransfer instead (see "Transfer-only categories" under Account names).


Transfers (move value between accounts)

Rule: any movement of value BETWEEN accounts uses a TRANSFER, not a transaction. createTransaction writes to one account. The moment value moves between two or more accounts — whether they belong to different actors or are two accounts of the same actor — use createTransfer (or createTransferTwoStep for a hold). The from legs debit, the to legs credit, and the whole thing is atomic.

A transfer has from (debit legs) and to (credit legs) — each an array of {accountId, amount, ...}. It is atomic.

# Immediate
createTransfer(accId="ws_xxx",
  from=[{ "accountId": "<source acc>", "amount": 200 }],
  to=[{   "accountId": "<dest acc>",   "amount": 200 }],
  comment="Budget reallocation", ref="transfer-q1")

# 2-step: authorize a hold, then complete/cancel
createTransferTwoStep(accId="ws_xxx", type="authorized",
  from=[{ "accountId": "<source acc>", "amount": 1000 }],
  to=[{   "accountId": "<dest acc>",   "amount": 1000 }],
  ref="payout-99")
createTransferTwoStep(accId="ws_xxx", type="completed", transferId="<tr id>")   # or type="canceled"

# Read
getTransfer(transferId="<tr id>", filter="id,amount,createdAt,comment")
getTransferByRef(accId="ws_xxx", ref="payout-99")
filterTransfers(accId="ws_xxx", from=1704067200, to=1706745600,         # window (unixtime)
                incomeType="debit", amount=200, oper="gte", limit=50)

Transfers between users (via their twin actors)

Every workspace user has a system "twin" actor that carries their accounts. To move value between users (or between a user and any other actor), you transfer between their twin actors' accounts — there is no "transfer to a userId" call. The flow:

  1. Find the user(s) by name → searchUsers(accId, query) (or getUsers) → get each userId.
  2. Resolve each user's twin actor → getSystemActor(accId, objType="user", objId="<userId>") → the user's actor (and its id).
  3. Find the account on each twin actor — resolve the account name first, then the currency (searchAccountNames→nameId, then searchCurrencies→currencyId), then getAccounts(actorId=<twin actor>) / getBalance(actorId, nameId, currencyId) to get the accountId (create it with createAccount if missing).
  4. Transfer between those accounts with createTransfer (the from/to legs reference the twin actors' accountIds).
# "Transfer 100 USD from Olena to Petro on their Wallet accounts"
searchUsers(accId="ws", query="Olena")        → userId 4210
searchUsers(accId="ws", query="Petro")        → userId 4310
getSystemActor(accId="ws", objType="user", objId="4210")  → actor A (Olena's twin)
getSystemActor(accId="ws", objType="user", objId="4310")  → actor B (Petro's twin)
getAccounts(actorId="<A>")  → Olena's Wallet accountId  accO
getAccounts(actorId="<B>")  → Petro's Wallet accountId  accP
createTransfer(accId="ws",
  from=[{ "accountId": "accO", "amount": 100 }],
  to=[{   "accountId": "accP", "amount": 100 }],
  comment="Olena → Petro", ref="o2p-1")

For more than two parties, add more legs to from/to (the totals must balance), e.g. split one debit across several credit legs. Multi-account moves on a single actor work the same way — just reference that actor's several accountIds in the legs.


Counters (Scylla-backed analytical tallies)

Counter accounts are accounts, but a special fast kind: their balances live in ScyllaDB and they keep no per-transaction history. They are cheap, high-throughput running counts/sums for analytics and anti-fraud — not an auditable ledger. Use the regular account + transaction tools when you need history; use the counters API for fast totals.

  • Addressed by (formId, actorRef, accountName, currency, incomeType) — not by account id.
  • counter = running sum (record deltas via amount); uniqCounter = dedupes by trsRef (the same event counted twice lands once).
# Record a mileage reading (running counter)
saveCounters(accId="ws_xxx", openAccounts=true, items=[
  { "formId": 42, "actorRef": "car-camry", "accountName": "mileage", "currency": "Km",
    "incomeType": "debit", "type": "counter", "amount": 45000 }])

# Override a counter to a fixed value
setCounters(accId="ws_xxx", items=[
  { "formId": 42, "actorRef": "car-camry", "accountName": "mileage", "currency": "Km",
    "incomeType": "debit", "amount": 50000 }])

# Read counter values
getCounters(accId="ws_xxx", items=[
  { "formId": 42, "actorRef": "car-camry", "accountName": "mileage", "currency": "Km",
    "incomeType": "debit", "type": "counter" }])

Two ways to get a counter, don't confuse them: a counter account created with createAccount(... counterType="counter") (addressed later by account id / actor+name like any account) vs the counters API above (saveCounters/getCounters, addressed by actorRef+accountName, bulk). Both are Scylla-backed and history-less; use the counters API for fast bulk analytics/anti-fraud counts, createAccount(counterType=…) to attach one to an actor.


End-to-end: car financial tracking

# 1. One-time workspace setup — createAccountPair creates the account-name + currency if
#    missing AND grants the caller pair access in one call. Do this for EVERY (name, currency)
#    you plan to attach to an actor — never skip it, even if the currency already exists.
createAccountPair(accId="ws", accountName="Purchase Value", currencyName="USD",
                  symbol="$", precision=2)    # → nameId <val>, currencyId 1
createAccountPair(accId="ws", accountName="Depreciation",   currencyName="USD")
                                                            # → nameId <dep>, reuses currencyId 1
createAccountPair(accId="ws", accountName="Maintenance",    currencyName="USD")
                                                            # → nameId <mnt>, reuses currencyId 1
createAccountPair(accId="ws", accountName="Mileage",        currencyName="Km",
                  symbol="km", precision=0)   # → nameId <mil>, currencyId 2

# 2. The actor (a simulator-actors task) — get its UUID, e.g. searchActors / createActor
car = "<car actor UUID>"

# 3. Attach accounts (the NAME carries the meaning; accountType defaults to fact)
createAccount(actorId=car, nameId="<val>", currencyId=1)                      # Purchase Value
createAccount(actorId=car, nameId="<dep>", currencyId=1)                      # Depreciation
createAccount(actorId=car, nameId="<mnt>", currencyId=1)                      # Maintenance
createAccount(actorId=car, nameId="<mil>", currencyId=2, counterType="counter")  # Mileage (counter)

# 4. Record value (find each account's id via getAccounts(actorId=car))
createTransaction(accountId="<acc_val>", amount=25000, comment="Initial purchase", ref="purchase-2023")
createTransaction(accountId="<acc_mnt>", amount=450,   comment="Oil change",        ref="service-jan-2024")
atomCreateTransaction(accId="ws", items=[                       # balanced depreciation entry
  { "accountId": "<acc_val>", "amount": -3000, "ref": "dep-2023-a" },
  { "accountId": "<acc_dep>", "amount":  3000, "ref": "dep-2023-b" }])
setAccountAmount(accountId="<acc_mil>", amount=45230)           # odometer reading (counter account)

# 5. Report
getAccounts(actorId=car)                                        # live balances per account
getAccounts(actorId=car, from=1704067200000, to=1706745600000) # turnover for the window (MILLISECONDS)
getAccountTransactions(accountId="<acc_mnt>", limit=50, offset=0)

Rank / filter actors by balance (a simulator-actors tool)

filterActors finds/ranks a form's actors by an account balance in one server-side query:

filterActors(formId=42, accountNameId="<aname>", currencyId=1,
             orderBy="balance", orderValue="DESC", withStats=true)   # top by balance
filterActors(formId=42, accountNameId="<aname>", currencyId=1, amountFrom=10000) # balance ≥ 10000

amountFrom = balance ≥, amountTo = balance ≤; CURRENT balance only (for turnover use getAccounts(from, to)). Full filter/data semantics live in simulator-actors.


Reference Documents

PathWhen to read
$CLAUDE_PLUGIN_ROOT/docs/entities/accounts.mdAccount types, income types, tree calculation, formulas
$CLAUDE_PLUGIN_ROOT/docs/entities/transactions.mdTransaction states, 2-step flow, atomic transactions
$CLAUDE_PLUGIN_ROOT/docs/entities/transfers.mdTransfer mechanics, holds, filtering
$CLAUDE_PLUGIN_ROOT/docs/entities/balances.mdBalance history, credit/debit split
$CLAUDE_PLUGIN_ROOT/docs/entities/counters.mdScyllaDB counters, time-series metrics
$CLAUDE_PLUGIN_ROOT/docs/user-flows/custom-car-form.mdComplete financial-tracking example

Tips

  • Amounts are real decimal values — amount: 500 is 500 USD; never scale by 10^precision.
  • Always call createAccountPair before createAccount for any (name, currency) you are about to use on an actor. It creates the account-name and the currency if missing AND grants you pair access — without it, the next getBalance/createTransaction/transfer 403s on any non-owner workspace. Prefer it over bare createCurrency + createAccountName.
  • accountType is the value type (fact default | plan | min/max/avg) and counterType selects amount (default) vs counter/uniqCounter; the account name carries the cash/expense/revenue meaning. There is no asset/liability/income enum. incomeType (debit/credit) is a read/counter dimension, not a createAccount arg.
  • Transactions use comment (not description); pass a stable ref for idempotency and to finalize 2-step holds.
  • Transfers use from/to arrays of legs {accountId, amount} — not fromAccountId/toAccountId.
  • setAccountAmount sets a fixed balance (correction); createTransaction moves it with history. Prefer transactions when audit history matters.
  • atomCreateTransaction for entries that must balance (double-entry).
  • getAccountTransactions requires both limit and offset; getTransactions requires currencyId + nameId.
  • Counters API = Scylla tallies, no history (analytics/anti-fraud); distinct from counter-type accounts.
  • Tags & triggers are actors (system forms Tags / AccountTriggers) linked to accounts via saveAccountActors — which replaces the linked set, so always scope it with formId and resend the full list when adding. Read them back with getAccounts(withTags=true / withTriggers=true); data triggers bind to a data field via saveDataFieldActorsByActor/Form.
  • Save tokens with filter on reads (getAccount, getAccounts, getBalance, getTransactions, getTransfer, getCurrencies, getAccountNames) — a comma-separated field allow-list (filter="id,amount,currencyId"), NOT a row filter.
  • Sharing an account (who can view/modify) → simulator-access; the actor itself → simulator-actors.

Frequently asked questions

What does the Simulator Finance AI skill do?

Simulator.Company financial management specialist. Use when the user wants to manage financial and metric accounts on actors (balances, counters, plan vs fact), record transactions, transfer value between accounts, work with currencies and account-name categories, track non-financial metrics, read balances and turnover, tag accounts for grouping, or set up account triggers (balance / transaction-count / data-field alerts). Activate when the user mentions "record transaction", "transfer funds", "account balance", "financial tracking", "depreciation", "expense", "budget", "counter", "mileage...

Why use Simulator Finance on TypingMind?

Because you install it once and use it with any model. Simulator Finance is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Simulator Finance in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/corezoid/simulator-ai-plugin/tree/main/plugins/simulator/skills/simulator-finance. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Simulator Finance?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Simulator Finance?

As many as you like. As long as a model supports skills, you can use Simulator Finance with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Simulator Finance AI skill free?

Yes. It is published on GitHub by corezoid under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇