// engineering notes, not a sales page
We built the ledger first.
The API came after.
Finex runs multi-currency wallets, card issuance, transfers, and
trading across forex, crypto, and stocks. Everything on this page
describes what is actually implemented in this codebase today,
including the parts we chose not to build the obvious way.
3
Markets traded
4
Currencies held
40+
API modules
0
Floats in balance math
01
Money movement
The ledger is the one part of this system that is not allowed to be
approximately correct. These are the guarantees it holds on every
write.
Row locked, not read then written
Every balance change locks the account row inside a database
transaction (SELECT ... FOR UPDATE) before reading
the balance it is about to change. Two requests against the same
account queue instead of racing, so a double conversion or a
double spend cannot happen no matter how fast they arrive.
Every entry carries a reference
A ledger post is idempotent on its reference. If the
same operation is retried, the original entry is returned
unchanged instead of a second one being created. A timeout on the
client side is never a reason for a user to be charged twice.
Integers only, in minor units
Balances and amounts are stored and moved as integers in the
smallest unit of the currency. There is no floating point anywhere
in the path between a request and a balance.
Overdraft checked against the lock
Insufficient funds is checked against the balance read inside the
same locked transaction that is about to spend it, not a value
fetched moments earlier for a preview or a quote.
Partial failure reverses itself
A trade or card issuance can fail after money has already moved: a
margin debit succeeds and a fee debit then fails, or a provider
call fails after a fee was charged. When that happens the entry
that already posted is reversed automatically, using the opposite
operation, tagged as a reversal, and linked back to the original
entry. The order is marked failed and the user's balance is
exactly what it was before the attempt.
02
Authentication
Sessions are designed to be revoked instantly, not to expire
eventually.
Argon2id for anything low entropy
Passwords, one time codes, and API secrets are hashed with
Argon2id, the current recommended default for password hashing,
not an older or faster algorithm chosen for convenience.
Revocation is immediate, not eventual
Every access token carries a unique id. Logging out, or changing a
password, blacklists that id in Redis right away. It is checked on
every authenticated request and every socket connection, so a
stolen token stops working the moment it is revoked instead of
whenever it happens to expire on its own.
One call ends every session
Logging out everywhere revokes every active session for an account
at once. A password reset does the same automatically, so a
compromised password cannot be used to keep an existing session
alive after it is changed.
More than one way to prove it is you
Passkeys through WebAuthn, TOTP, and single use backup codes are
all available as a second factor, alongside standard password and
one time code login.
Machines get their own credentials
Server to server access uses scoped API keys with their own rate
limits. A backend integration never authenticates as a user.
03
Abuse and access control
The API assumes it will be attacked, not just used.
Rate limits are per route, not global
Every route has its own ceiling, keyed to the caller's user id,
API key, or IP address, and backed by Redis so the limit holds
across every server instance handling traffic, not just the one
that saw the last request. A login attempt and a market data poll
are not the same risk and are not held to the same number.
IP restriction is opt in, on purpose
An account with no allowlisted IPs is unrestricted. Adding an
entry is what turns the restriction on. It can never lock out a
user who never asked for it, and it can be removed the same way it
was added.
Nothing from a webhook is trusted first
Every inbound payment webhook is verified with an HMAC-SHA256
signature over the raw request body, compared in constant time,
before anything in the payload is read or acted on.
04
KYC and compliance
Identity verification and audit trails, treated as part of the
architecture rather than a checkbox added afterward.
The API never touches raw identity documents
Documents and selfies upload directly from the client to storage
using a signed, time boxed upload scoped to that user's own
folder. The signing secret never reaches the client, and the API
rejects any submitted asset that is not genuinely from our own
storage account.
Limits scale with verification, not with trust
Trading and transfer limits are tied to which verification tier an
account has cleared. A new, unverified account cannot reach the
same limits as a fully verified one, regardless of how it behaves.
Every sensitive action leaves a record
Admin actions, suspending an account, reviewing a dispute,
adjusting a balance, are written to an audit log tied to the actor
who performed them. Access to those actions is governed by role
and permission based access control, not a single shared admin
flag.
05
Decisions we made
The obvious approach is not always the one we shipped. This is what
we weighed and why we chose differently.
Considered
Letting the client supply the fee for a trade or transfer,
since it already shows one in the quote.
Chose
Recomputing the fee server side from the live
rate at the moment of execution. A quote is a preview, never an
instruction.
Considered
Pinning our own certificate on mobile clients for the strongest
possible guarantee.
Chose
Pinning the certificate authority instead. Our
certificate rotates automatically every ninety days. Pinning it
directly would have quietly broken the app on schedule.
Considered
Cancelling a failed order and stopping there, since the order
status already reflects the failure.
Chose
Reversing any money that already moved before
marking the order failed, so a partial failure can never leave a
balance wrong even when the order record says the attempt did
not happen.
Considered
Requiring every account to opt in to an IP allowlist for
consistency.
Chose
Making it opt in per account instead. A
restriction nobody asked for is not a safety feature, it is an
outage waiting for the wrong login location.
Considered
Checking token validity by expiry alone, the simplest approach
and the one most APIs use.
Chose
An explicit revocation check on every request.
A token that is merely unexpired is not the same guarantee as a
token nobody has revoked.