Skip to content

feat(x402)!: facilitator as zero-share payee for upto#246

Merged
lgalabru merged 7 commits into
mainfrom
feat/upto-facilitator-payee
Jul 16, 2026
Merged

feat(x402)!: facilitator as zero-share payee for upto#246
lgalabru merged 7 commits into
mainfrom
feat/upto-facilitator-payee

Conversation

@lgalabru

Copy link
Copy Markdown
Collaborator

Summary

  • Cascades the x402 upto SVM design change (solana-foundation/x402 2fb26e63, spec in docs(svm): add upto SVM scheme specification x402-foundation/x402#2697, implementation in feat(svm): implementation reference for upto scheme  x402-foundation/x402#2694) across all SDKs that implement the scheme: Go, Kotlin, Python, Rust, Swift, TypeScript, plus the harness expectations.
  • The channel payee seat is now extra.feePayer (the facilitator) with a zero distribution share. This gives the rent sponsor a unilateral cleanup path — settle_and_seal(has_voucher = 0)distributereclaim — so a client/server pair can no longer strand the facilitator's rent by leaving channels open. receiverAuthorizer keeps payment authority as the voucher signer and no longer signs settlement transactions (voucher signing needs no fresh blockhash).
  • The distribution is always the explicit single-entry 100% payTo split; the payTo == receiverAuthorizer implicit-remainder branch is deleted everywhere. Channel PDA derivation moves feePayer into the payee seed slot.
  • Rust behavioral unlock: settle_actual_deferred no longer requires feePayer == receiverAuthorizer, since the fee payer is now the sole settlement-transaction signer.
  • x402 submodule bumped to 2fb26e63; vendored tarball rebuilt as x402-svm-2.16.0-paykit.2.tgz (lockfile refreshed).

Test Plan

  • Go: go test ./... green (incl. adapters/conformance); harness go-server builds.
  • Rust: cargo test — 933 tests green; no new clippy warnings.
  • Python: just test — 1336 passed; ruff + pyright clean.
  • Kotlin: just kt-test green. Swift: swift test — 202 tests green.
  • TypeScript: pnpm test — 514 passed; verified installed @x402/svm dist carries the new payee binding.
  • Harness e2e expectations updated (distribute account tail now includes the always-explicit payTo recipient ATA); e2e not run here — needs Surfpool.

Breaking Changes

  • Channels opened under the old payee binding derive a different channel PDA and fail verification against updated servers. Upgrade clients and servers together; drain in-flight channels before rolling out.
  • Note for the vendor recipe: just x402-vendor runs git submodule update --init, which resets a working-tree submodule bump back to the committed gitlink before packing — worth a follow-up fix.

Cascade the upto SVM scheme change from x402 2fb26e63 across all SDKs
(Go, Kotlin, Python, Rust, Swift, TypeScript) and the harness:

- The channel payee seat is now extra.feePayer (facilitator) with a zero
  distribution share, giving the rent sponsor a unilateral cleanup path
  (settle_and_seal with has_voucher = 0, then distribute and reclaim) so
  abandoned channels cannot strand its rent. receiverAuthorizer keeps
  payment authority as the voucher signer and no longer signs settlement
  transactions.
- The distribution is always the explicit single-entry 100% payTo split;
  the payTo == receiverAuthorizer implicit-remainder branch is gone.
- Channel PDA derivation moves feePayer into the payee seed slot.
- Rust: settle_actual_deferred no longer requires feePayer and
  receiverAuthorizer to be the same key, since the fee payer is the sole
  settlement-transaction signer.
- TypeScript: x402 submodule bumped to 2fb26e63 and vendored tarball
  rebuilt as x402-svm-2.16.0-paykit.2.tgz.

BREAKING CHANGE: channels opened with the old payee binding derive a
different channel PDA and fail verification against updated servers;
clients and servers must be upgraded together.
@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown

Greptile Summary

This PR cascades the upto SVM design change across all six SDKs (Go, Kotlin, Python, Rust, Swift, TypeScript) and the harness expectations. The core semantic shift is that the channel payee seat moves from receiverAuthorizer to feePayer (zero implicit-remainder share), the distribution is now always the explicit 100%-to-payTo split, and settlement transactions are signed only by the fee payer instead of the receiver authorizer.

  • Payee re-seat: feePayer holds the channel payee slot; receiverAuthorizer is now exclusively the Ed25519 voucher signer. Channel PDA derivation, open instruction, settle_and_seal payee argument, and distribute call are updated in every SDK.
  • Always-explicit distribution: The beneficiary == receiverAuthorizer → empty split branch is deleted everywhere; a single 100%-to-payTo entry is always produced.
  • Rust behavioral unlock: The settle_actual_deferred check that required feePayer == receiverAuthorizer is removed.

Confidence Score: 5/5

Safe to merge — the payee re-seat is applied consistently across all six SDKs and the harness, distribution logic is unified, and the deferred-settlement restriction is correctly lifted.

All six SDK implementations apply the same three-point change in lockstep: feePayer into the payee seat, always-explicit 100% distribution split, and settlement signed only by the fee payer. Test suites are comprehensively updated with correct distribution hashes, instruction counts, and new payee-slot regression guards.

No files require special attention — the change is mechanically uniform across all SDK layers.

Important Files Changed

Filename Overview
go/protocols/x402/upto.go Core Go server engine: expected payee changed to feePayer, distribution() now always returns explicit 100% split, SettleActual signs only with FeePayerSigner.
go/protocols/x402/client/upto.go Client: payee seed in FindChannelPDAForProgram and BuildOpenInstruction.Payee changed from receiverAuthorizer to feePayer; recipients always carries the explicit 100% split.
rust/crates/kit/src/x402/server/upto.rs Rust server: expected_payee to fee_payer, distribution() always explicit, sign_settlement_transaction uses only fee_payer_signer, settle_actual_deferred restriction removed.
python/src/solana_pay_kit/protocols/x402/upto/init.py Python server: payee set to fee_payer_pubkey, settle_and_seal payee and _distribution() updated. _EMPTY_DISTRIBUTION_HASH constant removed.
typescript/packages/pay-kit/src/adapters/x402-upto.ts TypeScript adapter: settle payee changed to #feePayer, #recipientSplits always returns explicit split.
harness/test/e2e.test.ts Harness expectations updated: +1 instruction for both settlement paths, distribute account count 11 to 12.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Server as Server (feePayer = channel payee)
    participant Chain as Solana Chain
    participant RA as ReceiverAuthorizer

    Note over Client,Chain: Channel Open
    Client->>Server: HTTP request
    Server->>Chain: "open(payee=feePayer, authorizedSigner=RA)"
    Chain-->>Server: channel confirmed

    Note over Client,Chain: Settlement
    Server->>RA: sign voucher
    RA-->>Server: signature
    Server->>Chain: settle_and_seal + distribute
    Note right of Chain: payTo gets 100% explicit split
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant Server as Server (feePayer = channel payee)
    participant Chain as Solana Chain
    participant RA as ReceiverAuthorizer

    Note over Client,Chain: Channel Open
    Client->>Server: HTTP request
    Server->>Chain: "open(payee=feePayer, authorizedSigner=RA)"
    Chain-->>Server: channel confirmed

    Note over Client,Chain: Settlement
    Server->>RA: sign voucher
    RA-->>Server: signature
    Server->>Chain: settle_and_seal + distribute
    Note right of Chain: payTo gets 100% explicit split
Loading

Reviews (5): Last reviewed commit: "chore(release): bump PayKit minor versio..." | Re-trigger Greptile

Comment thread go/protocols/x402/upto.go
lgalabru added 3 commits July 15, 2026 23:32
npm turned off the quick-audit endpoint (HTTP 410) and pnpm has no
release that targets the bulk advisory endpoint yet, so pnpm audit
fails on every branch. Treat exactly that failure as a warning while
still failing the job on real vulnerability findings.
The always-explicit payTo split adds one idempotent recipient-ATA
create before distribute in every settlement transaction (6
instructions with a voucher, 5 for zero-actual). Assert the new
layout and the recipient create; also apply rustfmt to the payee
regression test.
@lgalabru
lgalabru requested a review from EfeDurmaz16 July 16, 2026 10:10
EfeDurmaz16
EfeDurmaz16 previously approved these changes Jul 16, 2026
@lgalabru

Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

Copilot AI commented Jul 16, 2026

Copy link
Copy Markdown

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflict in typescript/pnpm-lock.yaml — the only conflict was the @x402/svm package entry (paykit.1 on main vs paykit.2 on this PR). Kept the PR's paykit.2 version. Merge commit: 1f8a00a.

@lgalabru
lgalabru merged commit 75f97c2 into main Jul 16, 2026
45 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.

3 participants