agentmesh · clearing interface

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.

version 1.0.0 revised 2026-08-07 implementations 2

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.

read this first

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.

absent

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.

reachable

The normal case.

unreachable

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

termmeaning
accountAn 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 keyAn 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.
balanceAn integer in the binding's own unit. It MAY be negative. In a binding with periods it is a figure for one period.
transaction identifierA 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 identifierA caller-chosen string that identifies one reservation. Distinct namespace from transaction identifiers.
settlementAny accepted value-moving operation, identified by its transaction identifier, with a state that is settled, pending or failed. A settlement is one transaction.
periodAn 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.

fieldrule
contractREQUIRED. The version of this document the binding implements.
implementationOPTIONAL. Free text naming the software and its version.
unitREQUIRED. Described in section 11.
operationsREQUIRED. 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.
finalityREQUIRED. immediate or deferred. See section 10.
notificationsREQUIRED. Lists poll and optionally callback. poll is REQUIRED.
fundingREQUIRED, and MAY be empty. The ways an account holder can put value into an account they hold. See below.
payoutREQUIRED. Whether value can leave the binding. Neither of our bindings offers payout, and there is no payout operation in this contract at all.
negative_balanceREQUIRED. Whether an account's balance may go below zero.
limitsOPTIONAL. Bounds on a single amount. Null means unbounded.
pending_max_secondsOPTIONAL. A hint, not a deadline. See section 10.
periodREQUIRED 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-balance and 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 pending MUST 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 retail for a settlement made by 6.8. For an operation with no payer, such as a credit or an adjustment, account is the account whose balance moved and amount is the size of that movement.
  • legs describes the whole movement, as an array of objects carrying account, a signed delta, and a binding-defined kind. REQUIRED of every binding that offers settle, 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.

an accepted operation that moved no value is not unknown

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.

what drawn means while a draw is pending

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.

a settlement is one transaction

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 report released: 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.

lazy expiry is permitted, lazy status is not

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

statuscodemeaning
403not_permittedThe caller may not talk to this binding at all.
404not_offeredThis operation exists in the contract and this binding does not offer it. Identical in meaning to its absence from operations.
404no_such_objectThe operation is offered. The object named by the path does not exist.
404no_such_routeNot an operation in this contract. A typo, or a caller built against a different contract version.
422invalid_requestMissing, malformed or contradictory arguments.
422insufficient_fundsavailable does not cover what was asked.
422terms_conflictA reused identifier with different terms.
422already_reversedThe named transaction already has a reversal under another identifier.
422not_reversibleThe named transaction is a correction, or is pending.
422closedThe target period or object is closed to further writes.
500internal_errorAn 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_offered and no_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.

  1. Reserving is not spending. Opening a hold moves no value. balance is unchanged by it. available is what falls.
  2. 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.
  3. A release returns only what was not drawn, and a hold can be released only once.
  4. 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.
  5. A hold's cap never falls below what has been drawn against it.
  6. 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.
  7. 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.
  8. History is append-only. Corrections append their inverse. Nothing is edited or deleted.
  9. 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.
  10. Settling never refuses for insufficient funds, and therefore a balance may go negative. A binding that cannot permit this declares negative_balance: false and the platform has no correct behaviour for it yet.
  11. 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.
  12. A reused hold identifier with different terms is refused, never silently reconciled.
  13. 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.
  14. 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.
  15. A settlement's final state is reached at most once. A settlement that reported settled never becomes failed, and one that reported failed never becomes settled. Changing a value that was already moved requires an explicit correction, not a state transition.
  16. 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.

operationkeywhat a repeat MUST return
resolve-accountnonethe same answer; it is a read
read-balancenonecurrent state; the read's own side effects apply once
read-summarynonecurrent state
read-settlementnonecurrent state
credittxnId, from the payment's identityapplied: false, no balance change
settletxnId, from the work's identityapplied: false, no balance change
reversetxnId of the reversalapplied: false, and the same amount
adjusttxnIdapplied: false, no balance change
open-holdholdId, the engagement's identityapplied: false plus the existing reservation when terms match; a refusal naming the existing terms when they do not
resize-holdnone; the target state is the keyapplied: false when the hold already has that cap and window
drawtxnId, the usage window's identityapplied: false, the same billed figure, and the same stored lines
release-holdholdIdapplied: 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: false is 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

designed, not observed

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

statemeaning
pendingThe binding has accepted the instruction and committed the value, but the value has not moved and the outcome is not known.
settledThe value has moved. Terminal, and can be left only by an explicit correction.
failedThe instruction will not complete. No value moved, and any commitment it held has been released. Terminal.
unknownThe 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 available at every admission check, ceiling check and hold. The binding reflects it in pending; the platform uses available rather than balance.
  • 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" and applied: false.
  • open further holds and draw further usage while an earlier settlement is pending, provided available still 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 platform carries 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 declares XIC, 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 period in its capability document, with a grain naming the partition and current naming the period that is open now. A binding without periods MUST NOT declare the field.
  • Carry period on every balance-bearing read: read-balance, read-summary, read-settlement and the two reservation reads, list-reservations and read-reservation. The value names the period the figures are for.
  • Carry period and opened_in on 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 period on 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:

accepted

The record is dropped, because the binding now holds it.

refused

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.

silent

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 applied means 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

an absent door is not a broken one

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-settlement composes 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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.