mergedhumanPR #38Payments
kavya-escrow-payments: conditional, arbitrated escrow plugin for the payments layer
The payments layer currently has two plugins: prepaid_credits (a 122-line debit/credit ledger) and the merged PR #21 streaming plugin (per-tick streaming with mid-stream cancel).
Author
@kavya-chennoju
github profile →- Status
- Merged
- Merged on
- Jul 1
- Branch
- hackathon/kavya-escrow-payments
Description
The pitch.
## Motivation The `payments` layer currently has two plugins: `prepaid_credits` (a 122-line debit/credit ledger) and the merged PR #21 `streaming` plugin (per-tick streaming with mid-stream cancel). Neither models the most common multi-party payment pattern on the agentic web: an **escrow** where a payer locks funds against work to be delivered by a payee, with a third-party arbiter empowered to settle disputes. Without this primitive, every test scenario that simulates a marketplace, a sub-contracting pipeline, or any "pay-on-delivery" workflow has to either trust the counterparty or model bespoke ledger glue. The `docs/layers/payments.md` wishlist explicitly calls out "escrow + dispute resolution" — no PRs had touched it. ## Design A new `escrow` plugin under the payments layer that adds the standard three-phase escrow flow: ``` FUNDED → DELIVERED → RELEASED (happy path) FUNDED → DELIVERED → DISPUTED → ARBITRATED (dispute path) FUNDED → REFUNDED (payer recalls undelivered) ``` Roles are bound to agent ids at `open_escrow` time. Only the payer can `release`/`dispute`/`refund`; only the payee can `deliver`; only the named arbiter can `arbitrate`, and only after the payer has disputed. Arbitration splits the locked amount in basis points (`payee_bps ∈ [0, 10000]`) — strict integer floor-division, no money created or destroyed. `pay()` is implemented as a one-call shortcut (open + immediate release) so the plugin satisfies the bare `Payments` protocol and any existing caller that just needs to move credits still works. ## Adversarial validators Four validators read the trace (`escrow:<kind>:<fields>` broadcasts) and check: 1. **`escrow_state_machine`** — every per-ref transition follows a legal edge (no `released` without `delivered`, no double-arbitrate). 2. **`escrow_role_binding`** — the broadcasting agent for every transition matches the role named in the originating `opened` event. 3. **`escrow_bps_in_range`** — every `arbitrated` carries `payee_bps` strictly in `[0, 10000]`. 4. **`escrow_no_payout_without_delivery`** — no `released`/`arbitrated` may settle for a ref that wasn't `delivered` first. Together they catch: forged delivery proofs, unauthorized releases, arbiter over-bounds, and the most common ambient failure of the `prepaid_credits` baseline — paying upfront with no delivery acknowledgment in the trace. ## Adversarial result The `escrow_marketplace` scenario drives 3 buyer/seller/arbiter triples: one happy, two dispute (3000 and 8000 bps). Same scenario, two plugins: - **`payments: escrow`** → all four validators **PASS** under seeds 42, 7, 1337. - **`payments: prepaid_credits`** → three of four validators **FAIL** with "no escrow lifecycle observed in trace — plugin lacks escrow protocol." (`escrow_bps_in_range` vacuously passes because there are no arbitrate events to range-check; that is the correct behavior — the rule is "every arbitrate ∈ range.") Tested end-to-end against the real `Simulator`, real `ScenarioRunner`, real `validate_trace`. No mocking past the plugin boundary. ## Determinism Tier 1: no wall-clock, no unseeded RNG. The plugin uses pure integer/float arithmetic and a deterministic state machine. The scenario factory uses fixed per-triple tick schedules. `test_escrow_scenario_deterministic_under_replay` pins **byte-identical** traces across two runs with seed 42, and `test_escrow_scenario_passes_across_seeds[42|7|1337]` confirms the same validator verdict across three seeds. ## Test rigor (70 new tests) - **42 unit tests** (`test_escrow.py`) — every state transition, every role-bound rejection, every state-machine guard, the `Payments`-protocol shortcut, and explicit conservation across release / arbitrate (parametrized over six bps values) / refund. - **4 Hypothesis property tests** (`test_escrow_properties.py`) — `@given(amount, bps)` over the full domain pins: (a) three-party ledger conservation, (b) split exact …
Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/38/head:pr-38
git checkout pr-38