Vote for your favorite SkillMD. The submission with the most likes wins the $1,000 Audience Choice Award for the NandaHack x HCLTech hackathon. Voting is open through September 25.Vote now →
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 avatar

@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 diff

Checkout locally

git fetch origin pull/38/head:pr-38
git checkout pr-38