Date: 2026-08-06
Status: Draft
Related:
btnl-reconciliation
(the reconciliation
that
delivers
the
clearing
data
this
RFC
consumes), accrual-engine
(the
accrual_engine
schema
this
RFC's ledger
lives
in),
A-4459/A-4462
(append-only
ledger
corrections).
Author: Andrew Lee (with Claude)
Bitnomial
is
the
DCM
and
owns
the
fee
program.
On
AIEX,
no
intraday
feed carries
the
fee:
FIX
drop
copy
ExecutionReports,
exchange
REST
/fills, and
BTP
are
all
fee-free.
The
only
source
of
the
actual
fee
charged
is
the Clearing
REST
API's
/executions
(commission
+
per-fee-type
fees breakdown),
which
is
post-trade,
eventually
consistent,
and
in
practice
a daily-report
cadence.
The
best
AIEX
can
do
intraday
is
guess.
Today's
code
pretends
otherwise:
btnl-trade-engine
computes
a
fee
from
our own
trading_accounts
rate
table
at
booking,
freezes
it
on
the
trade
row
as
if it
were
truth,
and
books
four
final
Fee
ledger
legs.
That
is
false
certainty —
the
internal
rate
table
has
no
authority
over
a
fee
program
Bitnomial
owns. This
RFC
replaces
it
with
an
honest
three-part
model:
a
frozen
estimate
at booking,
an
accrual
that
adjusts
risk
and
withdrawals
intraday,
and
a realization
step
that
books
the
venue's
actual
fee
into
the
cash
ledger when
the
clearing
report
arrives.
Every
stream
stays
append-only,
with
no exceptions.
A
note
on
the
current
state:
several
merged
Bitnomial
surfaces
(including
the btnl-execution-fees-match
recon
check
and
comments
asserting
fee
semantics) have
not
been
validated
against
real
clearing
data.
This
RFC
treats
them
as untested
scaffolding,
not
established
fact,
and
names
the
empirical
gates
in §Open
questions.
A fill and its fee are learned at different times from different sources: the trade happened (drop copy, intraday) and the venue determined its fee (clearing report, T+1). Model them as two records:
trades
rows
are
written
once
and
never
touched.
maker_fee_estimated
/ taker_fee_estimated
are
frozen
NOT
NULL
at
booking.
The
existing maker_fee
/
taker_fee
become
nullable,
mean
actual
fee
known
at booking,
and
on
AIEX
are
NULL
forever
—
actuals
are
never
backfilled onto
the
trade
row.
fee_determinations,
records
the
actual per-side
fee
when
the
clearing
report
is
matched:
(trade_id, account_id, fee, fee_breakdown, clearing_execution_id, report_date, determined_at, determination_seq).
A
restated
report
appends
a
superseding
row (determination_seq + 1);
nothing
is
ever
rewritten.
argMax
over determination_seq),
not
a
column.
Only
the
AIEX
edition
pays
this
join cost;
EP3
read
paths
are
unchanged.
This
dissolves
the
Option→Some
discomfort
entirely:
no
row
transitions
state, no
ReplacingMergeTree
version
races
on
trades,
and
restatements
are
ordinary appends
with
full
lineage
instead
of
a
special
case.
The
four
final
Fee
transactions
per
trade
stop
(AIEX
only).
Booking
instead appends
an
accrue
event
carrying
the
estimated
fee
to
a
fee-accrual ledger.
The
cash
ledger
(transactions
+
current_balances)
receives
only clearing-confirmed
fee
postings,
at
realization.
In
the
common
path
there
are zero
corrections:
the
estimate
lives
and
dies
in
the
accrual
ledger,
and system-correction://
(A-4459/A-4462)
stays
reserved
for
the
genuinely exceptional
case
of
Bitnomial
restating
an
already-realized
report.
The
accrual
ledger
mirrors
the
cash
ledger's
two-layer
shape:
an
append-only journal
(ClickHouse
fee_accruals:
accrue
/
release
/
park
events)
plus
a materialized
per-account
balance
(Postgres accrual_engine.fee_accrual_balances:
account_id, open_amount, sequence_number),
written
under
the
same
PG-commit-is-the-atomicity-boundary discipline
as
ax-transaction-engine.
Risk-engine
and
treasury
subscribe
to fee_accrual_balances
over
logical
replication
exactly
as
they
do current_balances,
and
compute:
equity = balance − open_accruals + …
withdrawable = balance − open_accruals − holds
One number, one new replica, no new mechanism. Without this, moving estimates out of the cash ledger would silently overstate intraday equity — and the sharper edge is withdrawals: an account could trade all day, withdraw its full balance, and go negative at realization.
The accrual item's lifecycle is the recon state machine:
fee_determination,
post
the
real
Fee
transactions
through ax-transaction-engine
(customer
debit
/
AX_FEE_ACCOUNT_ID
credit,
as today,
but
with
the
venue's
number),
release
the
accrual
—
all
anchored
on one
PG
commit;
Realization
is
per-item,
not
day-atomic
—
one
missing
execution
parks
one break
instead
of
holding
the
whole
day
hostage.
Each
day's
realizations
are batch-tagged
reference_id = clearing-report://<date>
so
the
cash
ledger records
which
report
caused
the
postings;
a
restated
report
is
a
new
batch that
supersedes
via
system-correction://.
The
operational
invariant:
the day
is
closed
⟺
open
accruals
for
that
day
are
zero
or
explicitly
parked. Statement
cut
gates
on
day-close;
an
aging
alarm
falls
out
of
the
same predicate.
Bitnomial
publishes
its
fee
schedule;
encode
it
codeslaw-style
(the ax-policy-fee
pattern,
PR
#3465)
as
a
versioned
estimator.
Every
estimate
is stamped
estimator_version
next
to
estimated_fee,
so
any
guess
is reproducible
after
the
schedule
changes.
Bias
conservative
—
estimate
at
the maximum
plausible
tier
—
because
withdrawable
(D3)
depends
on
the
accrual never
understating
the
eventual
charge.
The
existing btnl-execution-fees-match
check
stops
being
a
pass/fail
alert
and
becomes the
estimator-quality
monitor:
realized-vs-accrued
variance
per
account, feeding
schedule
updates.
The AIEX customer fee is exactly Bitnomial's determination. There is no Architect commission component booked on top, so there is no "known at booking" fee to realize immediately — the entire fee is venue-determined and flows through the estimate → accrue → realize lifecycle. (If a markup is ever introduced, it books final at fill through the existing path and only the venue component accrues; the model composes.)
accrual_engine;
crate
membership
is
openThe
accrual_engine
schema
(accrual-engine
RFC
D7,
PR
#3502)
is
the
declared home
for
accrual
ledgers,
each
landing
with
the
program
that
owns
it. fee_accrual_balances
(and
any
PG
bookkeeping)
lands
there.
Whether
the compute
joins
the
ax-accrual-engine
crate
is
left
open
(O5):
the
engine's lifecycle
is
per-period
(ACCRUED → APPROVED → PAID),
while
venue-fee
accrual is
per-fill
and
continuous
(open → realized | parked),
realized
into
the cash
ledger
rather
than
enqueued
to
program-payouts.
Shared
schema
home
and seam
philosophy:
yes.
Forced
abstraction
fit:
no.
/executions
is
the
only
venue
fee
surface: BitnomialExecution { commission: Option<Decimal>, fees: BTreeMap<String, Decimal> }
(rs/bitnomial/src/clearing/mod.rs),
with
the
documented
caveat that
commission
need
not
equal
sum(fees).
REST
/fills
and
FIX
drop copy
carry
no
fee.
btnl-trade-engine
computes
fee_rate × notional
from trading_accounts.maker_fee/taker_fee,
freezes
it
on
ChTradeRow (maker_fee/taker_fee
NOT
NULL),
and
transactions_from_trade (rs/btnl-trade-engine/src/positions.rs)
books
four
gross
Fee
legs against
AX_FEE_ACCOUNT_ID,
referenced
ax-fill://<trade_id>.
btnl-execution-fees-match
compares
venue
commission
vs
internal fee
legs
daily,
read-only.
Invariants
trade_fees_square, fees_are_zero_sum
assert
the
four-legs-per-trade
shape.
system-correction://<event-id>
+ reason
as
schema
convention
only;
no
writer
exists
(A-4462),
and ChTransaction
does
not
yet
carry
reason.
ClickHouse
(db/clickhouse/init.sql):
trades:
add
maker_fee_estimated Decimal128(12),
taker_fee_estimated Decimal128(12),
fee_estimator_version LowCardinality(String); maker_fee/taker_fee
become
Nullable
(EP3
keeps
writing
them;
AIEX writes
NULL).
fee_determinations
(new,
ReplacingMergeTree
keyed
by
(trade_id, account_id, determination_seq)):
actual
per-side
fee
+
breakdown
+ clearing
execution
id
+
report
date.
Read
via
argMax(determination_seq).
fee_accruals
(new,
append-only
journal):
(account_id, trade_id, event {accrue|release|park}, amount, estimator_version, sequence_number, timestamp, reference_id).
Postgres
(db/postgres/1.sql,
Atlas
declarative):
accrual_engine.fee_accrual_balances:
account_id PK, open_amount, sequence_number
—
the
O(1)
sum,
replicated
to
risk-engine
and
treasury.
Write
discipline
mirrors
ax-transaction-engine:
PG
balance
update
+
CH journal
insert
inside
one
PG
commit;
the
accrue
event
is
atomic
with
trade booking
(same
resume-token
transaction),
and
realization
is
atomic
with
the Fee
transaction
posting
and
determination
insert.
*_fee_estimated
on
the
trade
row
→
append
accrue
events
(per
side)
→ no
cash-ledger
fee
legs.
Crash-replay
regenerates
accruals
from
the frozen
estimate,
not
transactions.
open_amount.
Fills
API
serves
estimated_fee
labeled
as
such;
actual
is NULL
until
determined.
(trade_id, side)
→
append
fee_determination
→
post
Fee transactions
(venue
amount,
batch-tagged
clearing-report://<date>)
→ append
release.
Unmatched
venue
fee
→
book
against
the
account
keyed
to the
clearing
execution
id
and
park
a
break.
Accrual
open
past
N
days
→ aging
alarm,
park.
system-correction:// legs
for
the
cash
delta
(A-4462
machinery).
Public
Fill.fee
becomes
semantically
"actual,
when
known":
Option<Decimal> (EP3
always
Some,
AIEX
Some
only
post-determination),
plus
an estimated_fee
field.
Wording
must
not
leak
clearing
internals
per
the rs/sdk
hygiene
rule;
this
is
a
client-visible
contract
change
and
gets
its own
review.
fee_determinations
+
fee_accruals;
PG fee_accrual_balances.
Existing
AIEX
rows:
reinterpret
historical maker_fee/taker_fee
as
estimates
(backfill
*_fee_estimated
from them);
historical
determinations
optionally
synthesized
from
a
clearing /executions
backfill
(A-4386
§5.9).
trade_fees_square, fees_are_zero_sum)
rewritten
to
key
off
determinations
after
it.
fee_accrual_balances.
/executions
fetch
—
the A-4386
sync's
execution
stream,
or
an
interim
daily
job).
btnl-execution-fees-match
as
variance
monitor.
Land on aiex-demo first; soak the realizer signal-only (park-and-alert, no postings) until match rates prove the join key (O1), then enable postings.
/executions
row
be
matched to
our
(trade_id, side)
deterministically
—
does
it
carry
the
FIX
exec
id, order
id,
or
another
stable
key?
Verify
empirically
on
UAT (ax-admin-cli test btnl-clearing-probe)
before
any
schema
lands.
If matching
is
heuristic
(account/symbol/price/qty/time),
the
break
taxonomy and
soak
bar
both
grow
substantially.
/executions
is
eventually
consistent
with
no push.
What
signals
"day
D
is
complete"
—
a
fixed
hour,
a
clearing
statement artifact,
or
quiescence?
The
day-close
invariant
and
aging
threshold
N
need a
defined
baseline.
commission ≠ sum(fees)
residue):
book
to
the
customer
account
keyed
by clearing
execution
id,
or
to
a
dedicated
venue-fee-breaks
account
pending investigation?
Option<Decimal>
fee
vs
an
explicit {estimated, actual}
pair;
how
EP3
and
AIEX
share
one
type
without
leaking edition
internals.
ax-accrual-engine
crate
membership
(D7):
adopt
the
crate's lifecycle
machinery
for
a
per-fill
continuous
accrual,
or
share
only
the schema
home
and
the
transaction-engine
write
discipline?
Code
references
as
of
current
main.
Clearing
API
facts
per rs/bitnomial/src/clearing/mod.rs
and
the
btnl-reconciliation
SoW; treat
merged
Bitnomial
fee
behavior
as
untested
until
the
O1/O2
probes
run.