No base URL is configured. The payments subsystem does not exist on this mesh. Funding and balance surfaces are hidden and setting a price is refused at configuration time. A caller MUST NOT see an error suggesting something is down. Nothing is down.
What a clearing
implementation must do
A mesh operator can point AgentMesh at a settlement service. This is the contract that service answers: the operations, the invariants, the idempotency rules and the error taxonomy. Written from two implementations.
what this is
This is a seam, not a standard
This is not a protocol between parties. It is the seam between a platform and the settlement service that platform is configured to use. Both sides of the seam are operated by the same operator, and there is no negotiation across it. It has no domain of its own and asks nothing of anybody outside a single deployment.
This document is written from two bindings. The first is the credits rail. The second, an internal chargeback ledger, was built against the first version of the contract, and this version folds in what that binding found. Where the two disagreed, the disagreement is recorded rather than smoothed over.
Both bindings settle instantly. Section 10, asynchronous finality, is designed and not exercised. Section 15 lists everything else this document cannot yet claim.
1Scope
Everything that moves value lives behind the settlement service: balances, the resolution of an agent key to a paying account, funding, charges, corrections, and the reservations that back time-and-materials work. Nothing else in the platform opens a ledger.
An implementer who follows this document should be able to write a settlement service in any language, back it with anything from a Postgres table to an internal chargeback system to a bank, and have this platform work against it without changes.
It does not say how value is represented, what a unit is worth, whether money is real, whether funds can ever leave, or where value comes from in the first place. Those are the implementation's business and are declared, not assumed.
Terminology: the binding is the settlement service being implemented. The platform is AgentMesh, the caller. An account is the binding's unit of ownership, meaning whatever holds a balance.
The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, MAY and OPTIONAL are to be interpreted as described in RFC 2119.
2The switch has three states, not two
The platform decides whether to call a binding at all from one operator setting, a base URL. Three states follow from it, and they are not interchangeable.
The normal case.
A URL is configured and the binding does not answer. A broken till, which is not the same as an absent one. Priced paths MUST fail closed. Free traffic MUST be unaffected, because it never touches the binding.
An implementation cannot control which state it is in, but it must understand that the platform distinguishes them, because the third state is where the fail-closed rules in section 13 apply.
A fourth distinction sits inside the second state and is easy to miss. A binding that is reachable and answers 404 to an operation it does not offer is not broken either. Section 7 gives that answer a machine-readable code so the platform can tell it apart from a route that does not exist and from an object that does not exist.
3Transport, addressing and access
Both of our bindings are HTTP/JSON services. Requests and responses are JSON objects. Path parameters are percent-encoded. All amounts are JSON integers.
The wire forms in section 6 are normative for an HTTP binding. The operation semantics are the contract. A non-HTTP binding MUST preserve the semantics, the idempotency rules and the error taxonomy, and is free to choose its own framing.
Access control
Both of our bindings run on loopback and answer only local, unproxied callers.
Each checks two things and requires both: the peer address is a loopback
address, and no X-Forwarded-For header is present.
The second check exists because a reverse proxy also connects from loopback,
and the header it always adds is what gives away a request that came from the
internet. A request failing either check is refused with 403.
This is a co-location assumption, not an authentication scheme. A binding that is not co-located with the platform MUST authenticate its callers by some other means. This document does not specify one, and that gap is open question 1.
The requirement the loopback gate stands in for is this: only the platform may call the binding. A binding whose correction, funding or allocation doors are reachable from the network is a mint.
Health
A binding MUST expose an unauthenticated liveness check that answers success
without touching account state. Both of ours answer
GET /healthz. The health endpoint is outside the
access gate.
4Model
| term | meaning |
|---|---|
| account | An opaque string identifying something that holds a balance. The platform passes it through without interpretation. The binding assigns them and may reject identifiers it does not recognise. A binding MAY hold accounts that are its own machinery, such as a mint or a budget authority, and MAY refuse to settle, reserve against or adjust those. |
| agent key | An opaque string identifying a mesh agent. The binding maps agent keys to accounts. This mapping is the binding's, not the platform's, because the binding is where the question of who pays is decided. |
| balance | An integer in the binding's own unit. It MAY be negative. In a binding with periods it is a figure for one period. |
| transaction identifier | A caller-chosen string that identifies one movement of value. Every value-moving operation carries one. This is the idempotency key and it is the entire idempotency mechanism. |
| hold identifier | A caller-chosen string that identifies one reservation. Distinct namespace from transaction identifiers. |
| settlement | Any accepted value-moving operation, identified by its transaction identifier, with a state that is settled, pending or failed. A settlement is one transaction. |
| period | An optional partition of a binding's balances. See section 12. |
5Capability declaration
A binding MUST be able to say what it does not do. An internal chargeback ledger has no funding door and no payout. A payment rail may settle later rather than immediately. A binding whose balances reset must be able to say that too. The platform must be able to learn all of this without trying an operation and reading the failure.
A binding MUST answer a capability read. The wire form is
GET /internal/clearing/capabilities. The capability
read is REQUIRED of every binding and is not itself listed in
operations. Both bindings implement it, in about
thirty lines each, which is what makes keeping it REQUIRED reasonable.
the second binding's actual answer, as an example of the shape
{
"contract": "1",
"implementation": "agentmesh-chargeback 1.0",
"unit": { "code": "XIC", "display": "internal unit", "scale": 0, "peg": null },
"operations": [
"resolve-account", "read-balance", "read-summary", "read-settlement",
"settle", "reverse", "adjust",
"open-hold", "resize-hold", "draw", "release-hold",
"read-reservation", "list-reservations"
],
"finality": "immediate",
"notifications": ["poll"],
"funding": [],
"payout": false,
"negative_balance": true,
"limits": { "min_amount": 1, "max_amount": null },
"pending_max_seconds": null,
"period": { "grain": "month", "current": "2026-03" }
}
It is not a template for what a binding should say. A binding says what is true of itself.
| field | rule |
|---|---|
| contract | REQUIRED. The version of this document the binding implements. |
| implementation | OPTIONAL. Free text naming the software and its version. |
| unit | REQUIRED. Described in section 11. |
| operations | REQUIRED. Every operation from section 6 the binding offers. An operation not listed is not offered. resolve-account, read-balance, read-settlement and settle are REQUIRED of every binding. All others are OPTIONAL. |
| finality | REQUIRED. immediate or deferred. See section 10. |
| notifications | REQUIRED. Lists poll and optionally callback. poll is REQUIRED. |
| funding | REQUIRED, and MAY be empty. The ways an account holder can put value into an account they hold. See below. |
| payout | REQUIRED. Whether value can leave the binding. Neither of our bindings offers payout, and there is no payout operation in this contract at all. |
| negative_balance | REQUIRED. Whether an account's balance may go below zero. |
| limits | OPTIONAL. Bounds on a single amount. Null means unbounded. |
| pending_max_seconds | OPTIONAL. A hint, not a deadline. See section 10. |
| period | REQUIRED of a binding whose balances are scoped to a period, and MUST be absent otherwise. See section 12. |
5.1What an empty funding array means, and what it does not
funding describes the ways an account holder can put
value into an account they hold. An empty array means there is no such way, and
the platform MUST NOT render a purchase or top-up door.
An empty array does not mean value never enters the binding. The second binding's most important write is exactly the write that puts value in. Somebody with budget authority allocates to a cost center for a period, on the binding's own door, and no cost center can allocate to itself. That is why it is not funding in this document's sense and why declaring the array empty is honest.
The distinction a binding MUST preserve is the one about reach, not about origin. If the party who holds the account can put value in, that is funding and it MUST be declared. If value enters by a decision somebody else makes, that is the binding's own business and this contract has no operation for it.
A binding whose funding array is empty MUST NOT be assumed to have balances that
came from outside it. Where they came from is answered by
read-summary if the binding offers it, and by nothing
otherwise.
5.2What the platform must do about a capability it does not have
When an operation is not offered, the platform MUST NOT call it, and MUST NOT synthesize it locally.
- No open-hold, draw or release-hold. The platform MUST NOT claim a cap is enforced. It records the intended cap as evidence, rates metered usage, writes settlement records, and reports that nothing settled. The platform calls this evidence grade as opposed to enforced grade, and the two words travel in the engagement record so both parties can see which one they have.
- No credit, or an empty funding array. The platform MUST NOT render a purchase or top-up door, and MUST NOT answer with an error suggesting the door is broken.
- payout: false. No payout surface exists. The platform MUST NOT offer to move value out.
- No reverse or adjust. The operator correction surface is absent. This is a real configuration for a system where corrections happen in the finance system rather than here.
- No read-summary. The platform composes what it can from
read-balanceand does not render provenance or a statement. It MUST leave the missing figures out rather than setting them to zero, because zero is a different and false claim.
If the platform calls an operation the binding does not offer anyway, the
binding MUST answer 404 with the standard error body and the code
not_offered, and the platform MUST treat that
response as identical to "not declared". The platform relies on this: it probes
the reservation door and degrades to evidence grade on a
not_offered answer. That probe MUST remain valid,
because a caller may hold a stale capability document.
6Operations
Amounts are integers in the binding's declared unit. Every response to an
accepted value-moving operation carries
applied, a boolean meaning that this call changed
something as opposed to being a replay of one that already did. Every such
response MAY carry state. An absent
state on an accepted response MUST be read as
settled. A refusal is not an accepted response,
carries no state, and MUST NOT be read as a settlement.
6.1resolve-account
GET /internal/ledger/account-for-agent/{agentKey}
→ { "agent": "<key>", "account": "<account>" | null }
REQUIRED. null is an answer, not an error. It means
the key resolves to nobody: a guest, a sandbox identity, an unassigned agent.
The platform turns null into a refusal that names
the payable paths. The distinction between null and
an unreachable binding is load-bearing and appears throughout section 13.
The binding SHOULD answer this cheaply. The platform calls it on the admission path of every priced request. The first binding caches the whole map in memory for five minutes, so a just-linked agent may not resolve for up to five minutes. The second binding reads an indexed table and has no such delay. The cache is incidental. The cheapness is not.
6.2read-balance
GET /internal/ledger/balance/{account}
→ { "account": "acct_1", "balance": 5000000, "available": 4200000,
"reserved": 800000, "pending": 0, "period": "2026-03" }
REQUIRED of every binding.
- balance is what the account holds. Reserved and pending value is still in it, because it has not moved.
- available is what the account can still commit. This is the number every admission and ceiling check uses.
- reserved is the sum of what open holds still commit.
- pending is the sum of accepted but unsettled debits. An immediate
binding MAY omit it. An absent
pendingMUST be read as 0. - period is REQUIRED of a binding that declared a period, and MUST be absent otherwise. See section 12.
A binding that does not implement reservations MUST still answer
available, equal to
balance, so callers never special-case the field.
Reading a balance MUST be safe to repeat. It MAY have side effects: the first binding applies due grants before answering, and both bindings release expired reservation windows during a read. Those side effects MUST be idempotent.
6.3read-summary
GET /internal/ledger/summary/{account}
OPTIONAL. The account-facing view in one read: account,
balance, available,
reserved, pending,
reservations, totals,
funding, statement, and
period under the same rule as 6.2.
totals is a provenance split describing where this
balance came from and where it went. The exact figures are the binding's,
because what provenance means depends on how value enters. The first binding
answers granted, earned, spent, purchased and paid out. The second answers
allocated, earned, spent and corrections. Both are honest for the binding that
answers them.
This is not cosmetic. If value can ever leave, a binding that cannot
distinguish granted balance from earned balance leaks: grant free balance on ten
accounts, self-deal to one, cash out. A binding that declares
payout: false MAY answer zeros, but SHOULD answer
honestly.
totals MUST be net of corrections. A reversed charge
MUST stop counting as spend, and a reversed grant or allocation MUST stop
counting as granted or allocated. A ledger where the balance nets correctly but
the spend figure keeps quoting a refunded charge is a ledger a person checks a
refund against and does not believe. In a binding with periods this rule holds
within a period. A correction nets against the period it lands in, which under
12.2 is the period that is open when it is made, and it does not reach back. An
earlier period's totals therefore continue to include a charge that has since
been reversed. That is a property of periodic books rather than an exemption
from this rule, and a binding with periods MUST NOT restate a closed period to
make a correction net against its original. A reader who needs the whole picture
reads both periods, which is what the correction's own record makes possible.
statement is a bounded list of recent entries,
newest first. Both bindings default to 30 and clamp an explicit limit to 500.
The shape of an entry is the binding's. The platform renders what it is given.
6.4read-settlement
GET /internal/ledger/settlement/{txnId}
→ { "txn_id": "...", "state": "settled" | "pending" | "failed" | "unknown",
"amount": 250000, "account": "acct_1",
"opened_at": "...", "settled_at": "..." | null,
"failure": { "code": "...", "message": "..." } | null,
"legs": [ { "account": "acct_1", "delta": -250000, "kind": "charge" },
{ "account": "acct_2", "delta": 250000, "kind": "earn" } ],
"reversed_by": "r1" | null,
"period": "2026-03" }
REQUIRED of every binding, including immediate ones, because the platform must be able to ask without knowing which kind it is talking to. Both bindings implement it. The first composes the record from its own postings rather than keeping a second table beside them, and in doing so it puts a settlement it still stores as two derived transactions back together into the one record this operation describes.
The single-account shape of the first draft could not describe a two-party settlement, which is what every settlement in this contract is. The fields are therefore defined as follows.
- account MUST be the paying account. amount MUST be what the
payer paid, meaning
retailfor a settlement made by 6.8. For an operation with no payer, such as a credit or an adjustment,accountis the account whose balance moved andamountis the size of that movement. - legs describes the whole movement, as an array of objects carrying
account, a signeddelta, and a binding-definedkind. REQUIRED of every binding that offerssettle, which is every binding. The deltas of a settled settlement MUST sum to zero. - reversed_by names the correction that undid this settlement, or null. REQUIRED. Without it, learning whether a settlement still stands takes a second read the caller has no way to construct.
- period is REQUIRED of a binding with periods, and names the period the settlement landed in.
unknown means the binding has no record of this
transaction identifier. It is not an error and MUST be answered with 200. The
platform uses it to distinguish a call that never arrived from a call that
arrived and is working, which is exactly the question a timeout leaves open.
A binding MUST NOT answer unknown for an
identifier it has any record of, whatever that record says about value. The
case that forces this into the contract is a draw the cap clamped to nothing
(6.13): its lines are recorded at zero and marked capped, and nothing is
billed, so a binding that keys this read on value moved answers
unknown for a call it accepted. A caller that
timed out on that draw then reads the one answer that means "send it again".
Such a settlement is settled, with
amount of zero and an empty
legs array, which satisfies the requirement above
that the deltas sum to zero. No further state is needed and this contract does
not add one: accepted-and-moved-nothing is a terminal outcome, not a fifth
kind of in-flight.
An immediate binding answers settled or
unknown and nothing else.
6.5list-reservations
GET /internal/ledger/reservations/{account}
OPTIONAL. Result: account,
balance, available,
reserved, reservations,
an array of reservation records, and period under
the same rule as 6.2. This read carries a balance, so a binding whose balances
reset MUST say which period the figures are for here as well.
A reservation record carries hold_id,
account, amount (the cap
it commits), drawn (billed against it so far),
status (open or
released), ref,
memo, opened_at,
expires_at, released_at,
release_reason. Expiry MUST be applied before this
read answers.
A binding with periods MUST also carry, on the record itself:
- period, naming the period this reservation draws against now, or null once it is released, because a released hold commits nothing and there is no month it is holding.
- opened_in, naming the period it was agreed in.
A hold is not required to be scoped to a period, and neither of ours is: a cap
agreed in one period can run into the next and commit a budget nobody has
allocated yet, which drives available negative, and
invariant 11 permits exactly that. What a caller MUST be able to do is tell
which period's value a reservation is holding. The two fields differ exactly
when a cap has outlived the budget it was agreed against, and a binding SHOULD
derive them rather than storing them, since a stored period is wrong from the
first boundary the hold crosses. A binding without periods MUST NOT carry either
field, under the same rule as 6.2.
6.6read-reservation
GET /internal/ledger/reservation/{holdId}
OPTIONAL. The record from 6.5 plus a draws array,
oldest first, and period beside it under the same
rule as 6.5. 404 with the code no_such_object when
there is no such hold.
Each draw record carries hold_id,
txn_id, ts,
amount, meter,
unit, count,
per, per_unit,
pass_through,
upstream_receipt, and
capped.
The raw meter count, the divisor actually applied, and the unit price are all
stored and all returned, so a buyer can recompute
amount as
floor(count * per_unit / per) from counts it reads
off its own records. The divisor MUST be written out even when it is 1. Under a
time-and-materials arrangement with no deliverable to accept or reject, that
recomputation is the buyer's only protection, so the record must support it
rather than merely assert a total.
The platform reads drawn from this operation before
rating a new batch of usage, so the cap is measured over the whole engagement
rather than over one batch. The binding is the authority on what was
billed. The platform MUST NOT rate against a remembered figure.
A binding MUST count an accepted but pending draw in
drawn, and therefore in
reserved, from the moment it accepts it. A pending
draw that did not consume the cap would let a second draw commit the same cap
twice, and the whole point of a cap is that it cannot be committed twice. If
the draw later fails, the binding MUST restore
drawn.
6.7credit
POST /internal/ledger/purchase
txnId required
account required
amount required, positive integer
ref optional, what the funding was
simulated optional boolean
→ { "ok": true, "applied": true | false }
Value in, put there by the account holder. OPTIONAL, and absent when
funding is empty.
The transaction identifier MUST be derived from the identity of the payment, not the identity of the work. A redelivered payment notification must not mint twice. This is why funding is a separate door from settling. Sharing one door would let a retried payment deduplicate against a charge, or the reverse.
simulated marks value created with nothing behind
it. A binding that can mint without money MUST record that fact permanently
against the entry. Once a balance exists there is no other way to answer whether
it is real, and a ledger that cannot answer that is not worth keeping.
6.8settle
POST /internal/ledger/charge
txnId required
caller required, the paying account
owner required, the earning account
rate required, positive integer, what the owner earns
retail required, integer not less than rate, what the caller pays
ref, memo optional
spendAgentKey optional, for the platform's own usage rollup
→ { "ok": true, "applied": true | false }
REQUIRED of every binding. The difference between
retail and rate is the
platform's margin. The platform computes both and the binding records both. The
binding MUST NOT invent a margin and MUST NOT reject a settlement because
retail equals rate. A
metered draw settles with no margin at all.
The payer's debit, the earner's credit and any margin MUST be applied
together under the single transaction identifier, or not at all. A binding
MUST NOT apply one part and not another, and MUST NOT require the caller to
name the parts separately. This was open question 3 and it is now settled. The
first binding writes two transactions under identifiers it derives from
txnId, which makes undoing a settlement a two-call
operation the caller has to get right. That is a defect of the first
binding, not a permitted shape.
Settling MUST NOT be refused for insufficient funds. Admission was the gate, and by the time this is called the work has already been delivered. The admission check and the settlement are separated in time, so a caller can race itself slightly negative. That debt is the platform's cost of its own admission imprecision and is carried visibly rather than passed to the party that did the work.
Where an overrun must be refused, the refusal belongs before the work: at
open-hold, which refuses a cap the balance cannot
back, and at draw, which clamps at the cap. Both of
those refuse before anybody has done anything. A binding that cannot let an
account go negative MUST declare
negative_balance: false. The platform has no correct
behaviour for that case, which is open question 3 in section 16.
6.9reverse
POST /internal/ledger/reverse
txnId required, the identifier of the reversal itself
reverses required, the identifier being undone
reason required, non-empty
actor optional, who authorised it
→ { "ok": true, "applied": true | false, "amount": 250000 }
OPTIONAL. Undo a transaction completely, by recording its exact inverse. Rules a binding MUST preserve:
- Nothing is deleted or edited. A correction appends. The original stays readable, the correction names what it undid, and the balance is the sum of both. Editing history to make a number right produces a ledger that balances and cannot be believed.
- Undoing a settlement is one call. It follows from 6.8. A caller that knows a settlement's identifier has everything it needs to reverse it.
- Full reversals only. A partial refund of a metered window is a different rating of the same usage, not a half-undo. It belongs in a re-rate followed by a reversal of the difference.
- A correction cannot itself be reversed. If a charge should stand after all, it is charged again under its own identifier, so the history reads as two decisions rather than one that flickered.
- A reason is REQUIRED and stored. A correction nobody explained is indistinguishable from a bug.
- A transaction may be reversed once. A second attempt under a different identifier MUST be refused and MUST name the reversal that already exists. A second attempt under the same identifier is an ordinary replay.
Reversing a draw MUST give the reservation's cap back, once, while the
hold is open. A draw spends the cap as well as the balance, so returning the
value without returning the cap leaves the reservation permanently smaller than
it was agreed to be, and the engagement can then never bill the work that
replaces the window that was refunded. The arithmetic that says this is right is
available: a draw does not change it, so undoing one
must not change it either.
This was one of five defects the first binding carried at the time of the first draft. It has been fixed there. In a binding that still models a settlement as separate legs, only the payer's leg may restore the cap, because the hold is the payer's and restoring on both would hand the same value back twice. In a binding that follows 6.8 the question does not arise.
A released hold gets nothing back. Its remainder has already returned to what the account can spend and it admits no further work, so raising its cap would commit value to a reservation that can never bill it. The payer is still made whole. Work replacing the refunded window needs a new reservation.
6.10adjust
POST /internal/ledger/adjust txnId required account required delta required, non-zero integer, either sign reason required, non-empty actor optional
OPTIONAL. A correction with no single transaction behind it: goodwill, something
agreed off-ledger, a balance repaired after a bug. Either sign is legitimate.
This is as capable of taking value away as of giving it. Idempotent on
txnId.
6.11open-hold
POST /internal/ledger/hold
holdId required, the engagement's own identity
account required
amount required, positive integer
expiresAt required, an ISO 8601 instant
ref, memo optional
→ { "ok": true, "applied": true, "reservation": {...}, "available": 200000 }
OPTIONAL, and the first of the four reservation operations. Refused for a
non-positive or non-integer amount, an unparseable
expiresAt, an account the binding refuses to hold
against, and two cases worth naming:
- A reused holdId with different terms, meaning a different account or a different amount. This MUST be refused and the refusal MUST name the existing terms. Quietly keeping the first set would hide a caller bug behind a working answer.
- Insufficient available funds. The refusal SHOULD state the amount asked, the amount available, the balance and the amount already reserved, because those four numbers are what tells the caller which of several things went wrong.
A hold MUST be refused when available does not cover
it. A reservation the balance cannot back is not a reservation. The whole point
of committing a cap is that a generous cap costs the client the use of that
value. A hold moves no value. Idempotent on
holdId: reopening with identical terms returns the
existing reservation with applied: false.
6.12resize-hold
POST /internal/ledger/resize
holdId required
amount required, positive integer
expiresAt optional, defaults to the existing window
reason optional
→ { "ok": true, "applied": true, "reservation": {...}, "available": 100000, "delta": 500 }
- A raise MUST fit in what is still available, exactly as opening a hold must.
- A reduction releases the difference immediately. Value committed to a cap that no longer exists is value held for no reason.
- A hold MUST NOT shrink below what has already been drawn against it. That would be un-billing settled work.
- A released hold MUST NOT be resized back into existence. A new hold is opened instead, which is what puts the funds check back in the path.
Idempotent in a different sense from the others. Resizing to the amount and
window a hold already has MUST report applied: false
and change nothing. This matters because the caller replays approvals, and a
resize that always applied would let a replayed amendment walk a cap upward one
call at a time.
This is a separate operation from open-hold on
purpose. Opening a reservation and re-sizing one are different intents, and
open-hold's refusal of a reused identifier with
different terms is the guard that catches a retried formation. Collapsing them
trades that guard away for nothing.
6.13draw
POST /internal/ledger/draw
holdId required
txnId required, identifying the usage window being billed
caller required, and it MUST be the account that owns the hold
owner required, the earning account
lines required, at least one
ref, memo optional
each line: meter (required), amount (required, non-negative integer,
already rated by the caller), and optionally unit, count,
per, per_unit, pass_through, upstream_receipt
→ { "ok": true, "applied": true, "billed": 120000, "refused": 0,
"remaining": 680000, "exhausted": false, "lines": [ ... ] }
- Nothing is billed past the cap. Lines are taken in order and the last one may be cut short. A clamped line records what was actually billed and marks itself capped. A line with nothing left for it is recorded at zero and marked capped rather than dropped, so the record shows what the cap refused. A settlement record that quoted a full line while paying part of it would be precisely the dishonesty the record exists to prevent.
- A draw cannot exceed its hold. Units the provider incurs after the cap is reached are the provider's to bear. This is enforced, not described.
- A pass-through line MUST reference the upstream receipt it is passing through.
- The value movement of a draw is an ordinary settlement under 6.8, one transaction, with rate equal to retail equal to the amount billed. There is no platform margin on metered work.
- The binding is the authority on
drawn. The caller reads it back rather than tracking it.
Idempotent on txnId. A replayed draw MUST post
nothing and MUST return what it did the first time: the same
billed figure and the same stored lines, with
applied: false.
refused on a replay is computed against what the
replayed call asked for.
6.14release-hold
POST /internal/ledger/release
holdId required
reason optional
→ { "ok": true, "applied": true, "released": 300000, "reservation": {...} }
- A release returns only what was not drawn. Never the full cap.
- Release is idempotent and one-way. A second release MUST report
applied: false, MUST reportreleased: 0, and MUST NOT hand the remainder back twice. The failure this guards against is not a duplicated log line. It is value credited twice. - A released hold admits no further work. A draw against it MUST be refused.
- Nothing is posted either way, because nothing was ever posted to hold it. Releasing a claim is arithmetic on what is available, not a movement of value.
6.15Expiry
A hold has a window. Past the window the remainder MUST be released without
anyone asking. Both bindings apply expiry lazily on every read or write that
depends on it, and also sweep every five minutes for accounts nobody happens to
read. A binding MAY choose either mechanism or both. What is REQUIRED is that
available and reserved
never count a closed window.
Expiry MUST be applied before a hold's status is read, not only before
its available and
reserved figures are added up. That means before
draw, resize-hold,
release-hold,
read-reservation and
list-reservations look at status. The first binding
did not do this: it checked status alone, so a hold whose window had closed
kept accepting draws until the next sweep. A window that stops meaning anything
for the length of a sweep interval after it ends is not a window. This was open
question 4, it is settled, and it has been fixed. A draw against an expired but
unswept hold MUST be refused.
7Errors
An error response carries an HTTP status and a JSON body with at least an
error field holding a human-readable sentence, and a
code field holding a token from the vocabulary
below.
{ "error": "there is no reservation sow:eng:ops:summarize",
"code": "no_such_object" }
7.1Why the code exists
In the first draft a 404 did three different jobs. It meant that the route does not exist, that the object named does not exist, and that this operation is not offered by this binding. Nothing machine-readable separated them, and the platform's behaviour depends on which one it is.
The concrete case is the reservation probe. The platform probes the reservation door and degrades an engagement from enforced grade to evidence grade on a 404, which is correct when the binding has no reservation door and catastrophic when the 404 meant that one particular hold identifier is unknown. Read the second way, one missing hold silently downgrades every cap on the mesh to advisory. No amount of prose matching makes that safe, and matching on prose across bindings written by different people is not something this document is willing to require.
7.2The vocabulary
| status | code | meaning |
|---|---|---|
| 403 | not_permitted | The caller may not talk to this binding at all. |
| 404 | not_offered | This operation exists in the contract and this binding does not offer it. Identical in meaning to its absence from operations. |
| 404 | no_such_object | The operation is offered. The object named by the path does not exist. |
| 404 | no_such_route | Not an operation in this contract. A typo, or a caller built against a different contract version. |
| 422 | invalid_request | Missing, malformed or contradictory arguments. |
| 422 | insufficient_funds | available does not cover what was asked. |
| 422 | terms_conflict | A reused identifier with different terms. |
| 422 | already_reversed | The named transaction already has a reversal under another identifier. |
| 422 | not_reversible | The named transaction is a correction, or is pending. |
| 422 | closed | The target period or object is closed to further writes. |
| 500 | internal_error | An unexpected failure inside the binding. |
A binding MAY add codes for refusals of its own. A caller MUST treat a code it does not recognise as equivalent to the status alone.
7.3What a caller does with an absent code
Neither of the two bindings emits code yet. They
predate this section, and both answer prose only. A caller therefore MUST
tolerate its absence, and this document states how, so that two callers do not
invent two different fallbacks.
- A 404 with no code, at a path the caller believes is an operation of this
contract and which carries no object identifier, MUST be read as
not_offered. - A 404 with no code, at a path carrying an object identifier, is
ambiguous between
not_offeredandno_such_object. A caller MUST NOT make a decision that changes how work is priced or enforced on the strength of it. In particular, a caller MUST NOT downgrade an engagement from enforced to evidence grade on such an answer. It SHOULD re-read the capability document instead, which is unambiguous. - Any other status with no code MUST be read as the status alone.
A binding conforming to this version of the contract emits
code on every error response.
7.4Refusal messages
Refusal messages are load-bearing and SHOULD be specific. A refusal saying that
a transaction was already reversed by r1, or that
there is no transaction named, tells an operator whether to try something else
or stop. A generic failure strips exactly that. The platform passes refusal
bodies through to the operator surface verbatim for this reason.
Some operations in the first binding answer 200 with
ok: false rather than a 4xx. That inconsistency is
the first binding's and is not part of this contract. A refusal MUST carry a
4xx status.
8Invariants
These are what an implementation must preserve. They are stated as properties of the binding, not of any particular storage engine.
- Reserving is not spending. Opening a hold moves no value.
balanceis unchanged by it.availableis what falls. - Available equals balance minus what open holds still commit minus what
pending debits commit. Formally,
available = balance - sum(open holds: amount - drawn) - pending. A surface that shows only the total lets a person plan against value a purchase order has already spoken for. - A release returns only what was not drawn, and a hold can be released only once.
- A draw cannot exceed its hold. The sum of amounts drawn against a hold never exceeds that hold's cap at the time of the draw. A pending draw counts against the cap from the moment it is accepted.
- A hold's cap never falls below what has been drawn against it.
- Value is conserved. Every unit anywhere came from somewhere. Both bindings enforce this as double entry, with the binding's own mint or budget account running a deep negative balance by construction. The mechanism is theirs. The conservation property is the contract, and a binding MUST be able to demonstrate it. In a binding with periods, conservation holds over the sum of all periods, not within one.
- Amounts are integers and stay integers. There is no floating point anywhere in the value path. The unit has no sub-unit unless the binding declares a scale, and even then the wire integer is in the smallest unit.
- History is append-only. Corrections append their inverse. Nothing is edited or deleted.
- The parts of one settlement cannot be separated by a crash. A settlement is one transaction, and both bindings maintain balances inside the same storage transaction as the postings. A binding MUST NOT be able to lose one side of a movement.
- Settling never refuses for insufficient funds, and therefore a
balance may go negative. A binding that cannot permit this declares
negative_balance: falseand the platform has no correct behaviour for it yet. - available may be negative. A hold is checked against available when it is opened, but nothing prevents the balance falling afterwards through unrelated settlements, and in a binding with periods a hold agreed in one period can commit the next period's balance before anything has been put in it. Reserved value can therefore exceed the balance.
- A reused hold identifier with different terms is refused, never silently reconciled.
- Reads may write, but reads must be repeatable. Both bindings release expired windows during reads, and the first applies due grants. Any such side effect MUST itself be idempotent, so a caller polling a balance does not accumulate value.
- Totals are net of corrections. A reversed charge stops counting as spend, and a reversed grant or allocation stops counting as granted or allocated. In a binding with periods this holds within a period: a correction nets against the period it lands in, and an earlier period's totals are not restated.
- A settlement's final state is reached at most once. A settlement that
reported
settlednever becomesfailed, and one that reportedfailednever becomessettled. Changing a value that was already moved requires an explicit correction, not a state transition. - A period boundary moves no value. In a binding with periods, value does not lapse by posting.
Two of these surprised the author of the first draft and are called out because
a second implementer would not guess them. Invariant 10, that settlement never
refuses, looks wrong until you notice that the work has already been delivered by
the time it is called and that refusing would move the loss onto the party that
did the work. Invariant 11, that available may be negative, follows from 10 and
means a caller cannot treat available as a
non-negative quantity anywhere.
9Idempotency
Idempotency is a requirement on every implementation. It is not an accident of either of ours, and the platform depends on it in several places where there is no other protection against a duplicate. The mechanism is the same everywhere: the caller chooses a stable identifier derived from the identity of the thing being paid for, and the binding refuses to apply the same identifier twice.
| operation | key | what a repeat MUST return |
|---|---|---|
| resolve-account | none | the same answer; it is a read |
| read-balance | none | current state; the read's own side effects apply once |
| read-summary | none | current state |
| read-settlement | none | current state |
| credit | txnId, from the payment's identity | applied: false, no balance change |
| settle | txnId, from the work's identity | applied: false, no balance change |
| reverse | txnId of the reversal | applied: false, and the same amount |
| adjust | txnId | applied: false, no balance change |
| open-hold | holdId, the engagement's identity | applied: false plus the existing reservation when terms match; a refusal naming the existing terms when they do not |
| resize-hold | none; the target state is the key | applied: false when the hold already has that cap and window |
| draw | txnId, the usage window's identity | applied: false, the same billed figure, and the same stored lines |
| release-hold | holdId | applied: false, released: 0 |
- Different intents get different doors and different keys. Funding is keyed by the payment, settlement by the work, a hold by the engagement, a draw by the usage window, a release by the hold. Sharing one door would let a retried funding deduplicate against a charge, or a retried draw against the hold that funds it.
- A replay MUST be reported, not hidden.
applied: falseis the difference between a call that worked and a call that had already worked, and callers log and count on it. - Idempotency is per binding, forever. A binding MUST NOT expire an idempotency key on a schedule that a caller could outlive. Neither binding garbage collects transaction identifiers. In a binding with periods, a transaction identifier is unique across all periods, not within one.
resize-hold is the odd one. It has no idempotency key
of its own, and is instead idempotent with respect to the target state. Setting a
cap to 500 twice leaves the cap at 500. That is what stops a replayed approval
from raising a cap repeatedly. A binding MUST preserve this property even if it
chooses to add an identifier.
10Asynchronous finality
Both bindings that exist are instant ledgers, so nothing in current code exercises this section. It is here because the bindings that would really test this contract, a bank transfer or a chain, settle later or never, and retrofitting this afterwards is a rewrite rather than an addition. Read it as a design that has not met an implementation.
10.1States
| state | meaning |
|---|---|
| pending | The binding has accepted the instruction and committed the value, but the value has not moved and the outcome is not known. |
| settled | The value has moved. Terminal, and can be left only by an explicit correction. |
| failed | The instruction will not complete. No value moved, and any commitment it held has been released. Terminal. |
| unknown | The binding has no record of this identifier. |
A binding declares finality: "immediate" or
finality: "deferred". An immediate binding never
produces pending.
10.2On the response
Every accepted value-moving operation MAY carry
state. An absent
state on an accepted response MUST be read as
settled. This is what makes an immediate binding,
including both of ours, conformant without changing a line.
A refusal is not an accepted response. A 4xx carries no state, and a caller MUST NOT read the absence of state on a refusal as a settlement.
A deferred binding MUST carry state explicitly on
every accepted value-moving response.
10.3Asking
The platform learns the outcome by asking, with
read-settlement. Polling is REQUIRED to be supported
by every binding.
A binding MAY additionally declare
notifications: ["poll", "callback"] and call back to
a URL the operator configured. A callback is a hint to ask, never an
authority. The platform MUST re-read the settlement and MUST NOT change any
state on the strength of the callback body alone. This keeps callback
authentication out of the contract entirely, which is worth more than the round
trip it costs.
pending_max_seconds is a hint about how long
settlements normally take, so the platform can choose a sane polling cadence and
an operator surface can say something honest about how long to wait. It is
not a deadline. When it elapses, the platform MUST keep asking and MUST
surface the stall to the operator. The platform MUST NOT decide the outcome
itself. Deciding locally that a pending settlement failed is inventing a
settlement.
10.4What the platform may do while a settlement is pending
This is the part that has to be decided now, because every caller in the platform embeds an answer to it.
the platform MUST
- treat a pending debit as committed. It counts against
availableat every admission check, ceiling check and hold. The binding reflects it inpending; the platform usesavailablerather thanbalance. - report a pending debit to the payer as pending, never as spent, and a pending credit to the payee as pending, never as earned.
- retain the transaction identifier for as long as the settlement is not terminal.
- surface a failed settlement to the operator, and mark the associated usage or engagement record as unsettled.
the platform MUST NOT
- re-issue a pending instruction under a new transaction identifier. That is a double payment waiting for the first one to confirm.
- count a pending settlement as paid for any purpose that outlives it: payout eligibility, an earnings figure, a statement line labelled paid, or evidence of payment offered to a reputation system.
- reverse a pending settlement. A reverse naming a pending transaction MUST be
refused with the code
not_reversible. Cancelling something in flight is a different operation from undoing something that happened, and this contract does not have it. - undo the work. Delivery already happened.
the platform MAY
- deliver the work and answer the caller. Delivery is not gated on finality. In the shipped shape, settlement runs after success and off the request path, so there is nothing to gate. A deployment that wants to gate delivery on finality is choosing a different product, and this contract does not require it.
- retry the same call with the same identifier. That is a replay, and a
deferred binding MUST answer it with the same identifier,
state: "pending"andapplied: false. - open further holds and draw further usage while an earlier settlement is
pending, provided
availablestill covers them.
10.5Failure, and who carries the fact of delivery
On failed, the binding MUST have already released
whatever the pending settlement committed, so
available recovers without the platform doing
anything. For a failed draw, the binding MUST also restore the hold's
drawn figure, because it raised it when it accepted
the draw. The platform re-reads the reservation rather than computing what it
should now be.
A failed settlement leaves a question the first draft did not answer: work was delivered, and nobody was paid for it.
The binding's record is a record about money, and a settlement that failed is a true statement that no money moved. It is not a statement that no work happened, and a binding MUST NOT be expected to hold one. The platform's usage, engagement or attachment record is where delivery is written down. On a failed settlement the platform MUST keep that record, MUST mark it unsettled with the binding's failure reason attached, and MUST surface it to the operator. It MUST NOT delete it, and MUST NOT re-price or re-rate the work to make the number go away.
The platform MUST NOT automatically retry a failed settlement. A failure is a fact about the world, and quietly trying again is how a payment that was declined for a good reason gets attempted six times. This is different from retrying a settlement the binding never received, which is covered in section 13 and is required.
There is no operation in this contract for writing off a failed settlement, and no state that says a human decided to stop chasing it. That is open question 13.
10.6Holds are not deferred
Opening, resizing and releasing a hold MUST be immediate even on a binding whose settlements are deferred. A hold is a local claim on a balance, not a movement of value, so there is nothing for it to wait on. A binding that genuinely cannot make a claim immediately MUST declare that it does not offer the reservation operations, and the platform degrades to evidence grade rather than pretending a cap is enforced.
11Currency and unit
A binding declares its unit and the platform does not assume one.
- code: an identifier for the unit. The platform requires the
three-letter ISO 4217 shape because every currency field in the protocol
validates against it, and ISO 4217 reserves the X prefix for non-national
units, which is what both of our units are. The first binding declares
XCR, a platform credit. The second declaresXIC, an internal chargeback unit. - display: a human label.
- scale: how many decimal places the integer represents. Zero means the integer is the unit.
- peg: what one unit is worth in some external currency, or null. The first binding states that one credit is one micro-dollar, which makes conversion from USD micro-amounts the identity function and therefore removes rounding disputes entirely. The second states null, because an internal unit is worth nothing outside the company that issued it and inventing a rate would be making one up.
A binding MUST NOT invent an exchange rate. A charge in a currency the binding has no rate for is recorded and left unsettled, never converted.
11.1Unsettleable is not deferred
Left unsettled is not the same as left waiting, and this document says so because getting it wrong is a silent permanent loss.
The platform used to count a charge in an unpeggable currency as deferred, the same word it uses for a charge waiting on an agreement nobody has signed yet. One of those becomes billable when somebody signs. The other never becomes billable at all, and nothing was ever going to try it again. A deferral nothing can resolve is a permanent loss with a patient-sounding name.
A rail MUST therefore be able to say that a charge can never settle on it, distinctly from not yet. When it says so, the answer is final: the platform records the reason on the charge, stops offering it to that rail, and keeps the charge, because it remains a true statement of work delivered at a declared price. This is now the platform's behaviour and it is fixed.
11.2Where the platform still assumes XCR
The parameterization is nominal today. Both bindings settle correctly at the
clearing boundary. A binding declaring anything other than
XCR is then contradicted by the platform's own
display and validation, and the second binding's test suite pins exactly that.
The unit is a constant repeated in two packages plus a peg module. Fixing that is
out of scope for this document, but it is why the
unit declaration exists here rather than being
deferred.
12Periods
Some bindings hold balances that reset. The first draft had no vocabulary for this at all, and the second binding needed one on its first day.
An internal chargeback ledger keys balances by account and period. A period is an accounting month. A cost center's budget for March does not carry into April: at the boundary, March's unspent allocation simply stops being in the sum. There is no lapse transaction and no sweep, because there is nothing to sweep. April has its own row and nobody has allocated to it yet.
Two consequences make this a contract concern rather than an implementation detail.
- Conservation only holds across periods. Invariant 6 says every unit came from somewhere. In a binding with periods, that is true of the sum over all periods and is not true of any single period, where value appears at the start and vanishes at the end with no transaction on either side. A binding with periods MUST be able to demonstrate conservation over the whole ledger. It MUST NOT be expected to demonstrate it within one period.
- A caller cannot otherwise tell what the number it read means. A balance of 1000 that resets in nine days is not the same fact as a balance of 1000 that does not, and nothing in the first draft let a binding say which it was.
12.1What a binding with periods MUST do
- Declare
periodin its capability document, with agrainnaming the partition andcurrentnaming the period that is open now. A binding without periods MUST NOT declare the field. - Carry
periodon every balance-bearing read:read-balance,read-summary,read-settlementand the two reservation reads,list-reservationsandread-reservation. The value names the period the figures are for. - Carry
periodandopened_inon every reservation record, so that a caller can tell which period's value a cap is holding and which period it was agreed against. - Carry
periodon the response to every accepted value-moving operation, naming the period the transaction landed in. - Keep transaction identifiers unique across all periods.
- Move no value at a boundary. A period boundary is arithmetic, not a posting.
12.2Where a correction lands
A correction to a transaction in an earlier period MUST land in the period that
is open now, not in the period the original was in. A closed period is a fact
about a span of time that already ended, and rewriting it to make a number right
produces a set of books that balances and cannot be believed. Landing the
correction in the open period is also what it means in cash terms: budget comes
back now, not last month. A binding MAY therefore refuse writes to a period it
has closed, with the code closed. This is also where
6.3's netting rule stops.
12.3What this contract does not have
Period close, the statements a close produces, and the reporting of an overrun are the second binding's own doors and are not part of this contract. The platform never calls them. They are named here only so that an implementer with periods knows this document is not standing in their way.
13Failure and unreachability
What follows is what the platform currently does at each place it calls a binding. These are part of the contract in the sense that an implementer needs to know them. A binding that answers slowly at the admission path costs throughput, and a binding that answers 404 to the reservation door silently downgrades every cap on the mesh to advisory.
Deliberate, and load-bearing
- Admission for a priced call. Fails CLOSED. The platform resolves the
caller's agent key to an account and reads that account's
available. If the binding is unreachable at either step, the request is refused with a dependency failure and a message saying to try again shortly. An unreachable till does not mean free. If the key resolves to no account, the request is refused with the price quoted and the payable paths named, which is a different message because it is a different situation. - The public gateway's payment-required response. Fails CLOSED. An unreachable ledger produces a 402 saying so, not a free call.
- Setting a price with no binding configured. Refused at configuration time. An attachment cannot be given a nonzero rate on a mesh with no clearing URL, which is why the admission path never has to handle a priced call with no binding. That combination cannot be created.
- Opening a hold at engagement formation. Fails CLOSED. A priced commitment nobody could hold is not one.
- Resolving the buying account before reserving. Fails CLOSED. An unresolvable account at formation time is a 503, not a proceed.
- The metered usage door. Fails CLOSED on the value path. If the accounts cannot be resolved, or the reservation cannot be read, or the draw does not answer, the platform refuses and states plainly that nothing was billed.
- Approval-authority policy check. Fails CLOSED. A policy an attacker can bypass by knocking over the resolver is not a limit.
- Operator corrections. Fails CLOSED, and says nothing changed. A reversal or adjustment against an unreachable binding answers 502 saying the ledger could not be reached, so nothing was changed. An operator needs to know whether to retry.
Deliberate, and fail-open
Settling after a successful call. Off the request path, and durable. Settlement runs after the work succeeded. The answer already given is never retracted, so nothing about it is allowed to fail the request. This used to be one un-awaited POST with no record, which meant a charge could simply vanish if the binding was down for the second it took.
It is now durable, and the rule it establishes is normative for anyone building the platform side of this seam. The attempt MUST be written down before it is posted, and retried with a widening gap until the binding accepts it, including across a restart. Three outcomes are kept apart because they are three different facts:
The record is dropped, because the binding now holds it.
The binding understood the charge and said no. Repeating it gets the same answer, so the attempt ends at once with the binding's own words recorded against it.
Unreachable, timed out, or a 5xx. A till that is down and not a charge that is wrong, so it waits and is tried again.
An attempt retried past the point where the next try is worth less than somebody knowing is abandoned with a reason. Abandoned records stay, and are served on an operator surface. Off the request path is not the same as best effort, and a charge that can never be collected is a fact somebody has to be able to find. Retrying is safe only because the transaction identifier is the work's identity and every binding is required to be idempotent on it. A platform that retried under fresh identifiers would double-bill.
- Forwarding rated charges to the billing rail. Awaited, off every request
path. Its
appliedmeans the binding has the charge. It used to mean the charge had been handed to the rail and was always true, which mattered because the platform writes that answer back onto the charge as settled and never offers a settled charge again. One POST that did not arrive produced a charge permanently recorded as collected and permanently uncollected. A charge the binding did not take now stays unsettled and is offered again the next time its window is rated. - Releasing a hold. Fails OPEN, best effort. The sweep will catch the hold, and expiry will catch it regardless. This is safe only because release is idempotent on the binding's side.
- Resizing a hold. Fails open on absence, closed on refusal. No binding configured is a no-op success. A 404 meaning the binding has no reservation door is also treated as success and the cap stands as a recorded intent. An explicit refusal or an unreachable binding is an error. Section 7.3 is what makes the middle case safe to read.
- Reserving a cap with no binding. Records intent. The engagement is created at evidence grade rather than enforced, and both parties can see which they have. This is the honest answer rather than a failure.
- A deployment that meters but does not bill. Every rated charge stays recorded and unsettled. That is the honest state, not a failure.
Reads, and the rule about absent doors
A 404 carrying not_offered, or an unambiguous
absence under 7.3, MUST be reported to the caller as an absent
capability, and MUST NOT produce an error implying the binding is down or
that the request was wrong.
- The account balance surface. No binding configured answers 404 saying credits are not enabled on this mesh. An unreachable binding answers 500 saying the ledger could not be read right now. Absent and broken get different words on purpose.
- The summary read. A binding that does not offer it answers 404, and the platform composes what it can from the balance read every binding must answer. Provenance and the statement are left out because they were never given, not set to zero, which would be a different and false claim.
- Funding. A binding that answers 404 at the funding door is stating that it has no funding door, which is a configuration somebody chose and gets the same answer as a mesh with no clearing service at all. An unreachable binding during a funding attempt answers 502.
- The platform's capability response. Reports whether a clearing URL is configured. It reports configuration, not reachability, and it does not yet proxy the binding's own declaration. A console therefore cannot tell the difference between a mesh with clearing configured and working and a mesh with clearing configured and dead. That is a platform gap, listed in section 15.
14What is incidental to our implementations
Collected here so a third implementer knows where it is free.
- The loopback plus no-forwarded-header gate. The requirement is that only the platform can call the binding.
- HTTP and JSON. The requirement is the operation semantics.
- The
/internal/ledger/path prefix and the operation names on the wire. - Double entry as the mechanism. The requirement is conservation.
- A mint or budget account that runs a deep negative balance. That is how both bindings make issued value and margins balance to zero.
- Grants, and their application during a balance read. Entirely the first binding's. A grant is not funding under section 5.1's reach rule, whatever it puts in, so the first binding declares the purchase door as its funding and reports the grant schedule under a field of its own.
- Allocation, period close, statements and overrun reporting. Entirely the second binding's.
- SQLite, and the five-minute sweep interval and cache lifetimes.
- The exact composition of the summary response and of
totals.
Two shapes are normative rather than incidental: a settlement is never stored
as two independently identified legs, which 6.8 forbids, and a refusal is never
returned as 200 with ok: false, which 7.4
forbids.
15Limits of this document
- Both bindings settle instantly. Section 10 is designed and not exercised. No code anywhere has produced a pending settlement, polled one, or handled a failed one. The first rail that settles later will find things in section 10 that are wrong, and that is expected.
- Neither binding emits the error codes of section 7. Both answer prose only. Section 7.3 exists so callers behave consistently until they do.
- The first binding still stores a settlement as two derived
transactions, which 6.8 forbids.
read-settlementcomposes them into the one record this document describes, so a caller reads the shape the contract states, but undoing a settlement there is still two calls. - The platform is not unit-agnostic. Section 11.2 says where it still assumes XCR.
- The platform does not proxy the binding's capability document. It reports its own configuration instead.
- There is no authentication scheme for a remote binding. Section 3 assumes co-location.
- Neither binding has been driven under concurrency. Both serialise inside one storage transaction per operation. This document states no ordering requirement, and a distributed binding will need one.
- Both bindings share a lineage. Both are ours, both are TypeScript and both are SQLite, so a binding built on a different stack may find assumptions here that neither of them tested.
16Open questions
Settled since the first draft, and recorded so nobody reopens them: whether a settlement is one thing or two (6.8, one), whether a draw against an expired but unswept hold is refused (6.15, it is), and whether a machine-readable error vocabulary belongs in this contract (7.2, it does).
still open
- Authentication for a non-co-located binding. The current gate assumes the binding runs on the same host. There is no scheme for one that does not, and a correction door reachable from the network is a mint. Open because neither implementation needed it, so anything written here would be a guess.
- Where the capability declaration lives. Served by the binding, which ties discovery to the binding's availability, or configured beside the URL by the operator, which lets it go stale. Section 5 assumes the former. Open because the failure mode of the former, a binding that is down and therefore cannot say what it does, has not yet happened to anybody.
- What a binding that cannot go negative does. A system with a hard limit
cannot honour invariant 10. The platform has no behaviour for a settlement
refused after delivery, and inventing one means deciding who eats the loss.
Both bindings declare
negative_balance: true, so neither forced the question. - Whether a binding should be able to declare that it is the durable one. Section 13 makes durability the platform's job, which is right when the platform is the only party that knows a charge was owed before the binding has heard of it. A binding with its own durable intake could take responsibility instead. Nothing in the contract lets it say so.
- Whether pending belongs in the balance read or in a separate operation. Putting it in the balance read means every binding answers a field most of them will always report as zero. Both of ours report zero.
- Cancellation of a pending settlement. Section 10 forbids reversing one and provides no cancel. A rail where an instruction can genuinely be recalled before it lands would want one, and adding it later means adding a state.
- Multi-currency bindings. The contract assumes one unit per binding. A binding holding balances in several units has no way to say so, and the platform has no way to ask which one an account is in.
- Whether the usage rollup fold belongs in clearing at all. The first binding folds a settled charge into the platform's own spend visibility layer from inside the clearing service, which means every binding would have to reimplement a piece of platform bookkeeping that has nothing to do with settlement. The second binding does not do it, and the platform's spend surfaces are correspondingly thinner against it.
- How value gets in when the account holder cannot put it there. Section 5.1 says clearly that an empty funding array is about reach and not about origin. Whether the platform should be able to see that value entered at all, without being able to cause it, is not settled. Today it cannot.
- Whether an operator surface should exist for a binding's own doors. The second binding has allocation, period close and statements, and an operator reaches them with a script. The platform's operator console knows nothing about them and this contract gives it no way to find out they exist.
- Ordering guarantees. Nothing here says what happens when a draw and a resize arrive concurrently for the same hold, or two draws for the same hold. Both bindings serialise inside one storage transaction per operation, but the contract states no requirement, and a distributed binding will have to.
- How much of the period model belongs here. Section 12 requires a binding with periods to say so and to label its figures. It does not let the platform ask for a past period, and it says nothing about what a platform surface should show when the period it is displaying is about to end.
- What happens to a settlement that permanently failed. Section 10.5 says the platform keeps the delivery record, marks it unsettled and surfaces it. It does not say what an operator does next. There is no write-off, no forgiveness, and no state meaning that somebody decided to stop chasing it.