Skip to content

feat(spend): one-way payment-channel spend rail + OpenAI gateway#12

Merged
drewstone merged 3 commits into
mainfrom
feat/ungameable-mechanism
Jun 13, 2026
Merged

feat(spend): one-way payment-channel spend rail + OpenAI gateway#12
drewstone merged 3 commits into
mainfrom
feat/ungameable-mechanism

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

What & why

Replaces the bearer-key spend model with a one-way payment channel so an operator can never bill more than the consumer signed — over-billing is impossible by construction, not merely detectable. The consumer keeps a vanilla OpenAI experience (base_url + api_key); a local gateway signs the crypto invisibly.

Design memo: docs/specs/spend-rail.md.

How it works

  1. One wallet signature — the holder signs SpendPermit{lotId, sessionKey, maxTokens, expiry}, delegating a fresh ephemeral session key to draw down one lot (capped, expiring, revocable).
  2. Per-request voucher — the session key signs SpendVoucher{lotId, sessionKey, servedCumulative} acknowledging cumulative tokens served. The operator serves request N+1 only once the voucher acks everything through N, so its exposure is bounded to a single unacknowledged request.
  3. SettlementsettleSpend(permit, holderSig, servedCumulative, voucherSig) verifies both the holder permit (bound to the lot's current holder, so a resold lot's channels die at transfer) and a session-signed voucher, settling only up to what the session key acknowledged. Cumulative + monotone → flush at any cadence, replays no-op, keeper-friendly.

Changes

  • Contract (SurplusSettlement.sol): SpendKeyAuthSpendPermit/SpendVoucher; settleSpend rewrite; spendPermitDigest/spendVoucherDigest views; spendSettled/spendRevoked keyed by permit digest; revokeSpendKey. (Also: renamed a local bookFee to clear a shadowing warning against main's bookFee().)
  • Operator (spend.rs): channel-model serve + settle; new POST /v1/spend/ack records a trailing voucher with no serve, so the last request of a channel is settleable too (closes a serve-for-free gap). Metrics from main preserved.
  • Gateway (surplus-gateway bin): holds the session key, signs vouchers, exposes a vanilla OpenAI surface; sends the trailing ack after each response. Run locally for zero trust.
  • App (Portfolio.tsx): ApiKeyMint delegates a fresh session key and shows the one-time gateway run command; SPEND_PERMIT_TYPES replaces the old key-hash typed data.
  • Cross-stack EIP-712 parity pinned (Rust digest test == contract pins).
  • e2e (spend-e2e.mjs/spend-live.mjs) rewritten to the channel model.

Verification

  • forge test: 100 passed (incl. the Solvency invariant exercising the channel-model spend(); a temporary isolation test confirmed spendsSettled == 1).
  • spend-e2e.sh against anvil: proven end-to-end — one signature → session key, two vanilla OpenAI calls served, a forged (non-session) voucher refused, both requests settled via the trailing ack, lot debited exactly by served tokens on-chain.
  • Rust workspace + chain + mesh features build; operator tests pass; app typechecks + builds; router-bridge 26 tests pass.

Out of scope / follow-ups (documented in the memo)

TEE-attested inference for authenticity trust; full Tor deploy; per-holder session limits to harden the residual one-request griefing bound.

Replace the bearer-key spend model with a one-way payment channel so an
operator can never bill more than the consumer signed — over-billing is
impossible by construction, not merely detectable.

- Contract: SpendKeyAuth -> SpendPermit{lotId,sessionKey,maxTokens,expiry}
  (holder delegates a session key with ONE signature) + SpendVoucher
  {lotId,sessionKey,servedCumulative} (session key acks cumulative served).
  settleSpend verifies BOTH the holder permit (binds to the lot's current
  holder) and a session-signed voucher, settling only up to what the session
  key acknowledged. Cumulative + monotone, permissionless, revocable.
- Operator: spend.rs serves request N+1 only once the voucher acks everything
  through N (exposure bounded to one request) and settles the latest voucher.
  New /v1/spend/ack records a trailing voucher with no serve, so the LAST
  request of a channel is settleable too (closes a serve-for-free gap).
- Gateway: new `surplus-gateway` bin holds the session key, signs vouchers
  invisibly, and exposes a vanilla OpenAI surface (base_url + dummy api_key) —
  run locally for zero trust. Sends the trailing ack after each response.
- App: ApiKeyMint delegates a fresh session key and shows the one-time gateway
  run command; SPEND_PERMIT_TYPES replaces the old key-hash typed data.
- Cross-stack EIP-712 parity pinned (Rust digest test == contract pins).
- e2e rewritten to the channel model and proven end-to-end on anvil: forged
  (non-session) voucher refused; lot debits exactly by served tokens.
- Design memo: docs/specs/spend-rail.md.
The branch merge silently dropped main's CI setup (forge soldeer install in
the contracts and parity jobs, protoc install in the zk job). This work does
not touch CI; restore main's workflow verbatim so CI is unchanged by this PR.
@drewstone drewstone merged commit 4e56f82 into main Jun 13, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant