Cutting a 3-second response to 200ms usually has nothing to do with making the query faster.
The endpoint returned the amount a user could actually spend, and it computed that at read time: walk the ledger entries for the account, subtract holds, apply pending settlements, return a number. Correct by construction, and slower every month the account stayed alive, because the cost tracked history instead of the request.
No index fixes that. A read-time aggregate over an append-only ledger is work you can move, not shrink.
So I moved it to write time. The balance became a stored row updated in the same transaction as the entry that changes it: same commit, same lock scope, so it cannot disagree with the ledger it is derived from. The read became a single primary-key lookup. 200ms is not a faster version of the old path. It is a different path.
The part that takes judgement is trust. A derived number you serve to users needs an independent recomputation out of band: rebuild from entries, compare, alarm on drift. Not to repair it silently, but to learn the invariant broke before a customer tells you.
Most latency work is deciding when the work happens. Read time, write time, or background. Pick deliberately, because that choice sets the ceiling long before query tuning does.
I write the longer version of these trade-offs up here: https://polycratia.com/c/04e5eec6
If you keep a derived balance, I'd check one thing: whether yours commits with the ledger write, or gets rebuilt by a job...