Skip to content

Feat/batch settlement go sdk#2164

Closed
CarsonRoscoe wants to merge 17 commits intox402-foundation:batched-settlementfrom
coinbase:feat/batch-settlement-go-sdk
Closed

Feat/batch settlement go sdk#2164
CarsonRoscoe wants to merge 17 commits intox402-foundation:batched-settlementfrom
coinbase:feat/batch-settlement-go-sdk

Conversation

@CarsonRoscoe
Copy link
Copy Markdown
Contributor

Description

Tests

Checklist

  • I have formatted and linted my code
  • All new and existing tests pass
  • My commits are signed (required for merge) -- you may need to rebase if you initially pushed unsigned commits
  • I added a changelog fragment for user-facing changes (docs-only changes can skip)

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

@CarsonRoscoe is attempting to deploy a commit to the Coinbase Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added go sdk Changes to core v2 packages examples Changes to examples typescript labels Apr 30, 2026
@CarsonRoscoe CarsonRoscoe force-pushed the feat/batch-settlement-go-sdk branch from e5a2208 to 79d71bb Compare May 1, 2026 01:04
@up2itnow0822
Copy link
Copy Markdown
Contributor

Nice to see Go parity landing for batch-settlement.

From the paid MCP provider side, the main compatibility risk is not the happy path. It is whether TS and Go clients leave operators with the same channel evidence after deposit, voucher signing, corrective 402 recovery, claim, refund, and final settlement.

We added an AgentPay MCP compatibility recipe that tracks the PR #2164 shape:

  • shared channelId proof across TS and Go,
  • CHANNEL_SALT recorded as part of channel identity,
  • the initial, recovery-refund, and full e2e phases preserved as operator-visible proof rows,
  • EVM_VOUCHER_SIGNER_PRIVATE_KEY treated as scoped payerAuthorizer delegation,
  • receiver authorizer and facilitator signer logged separately,
  • refund rows that include outstanding signed max claimable and claim-before-refund behavior.

Docs/test proof is in our AgentPay MCP PR: up2itnow0822/agentpay-mcp#8

Direct commit: up2itnow0822/agentpay-mcp@b241396

This should give paid MCP providers a concrete checklist for verifying multi-SDK batch-settlement before they rely on repeat-call vouchers in production.

@phdargen phdargen force-pushed the batched-settlement branch from f61da35 to 4b31597 Compare May 1, 2026 02:26
@phdargen phdargen self-assigned this May 1, 2026
@CarsonRoscoe CarsonRoscoe force-pushed the feat/batch-settlement-go-sdk branch from 53c87ba to 74c051a Compare May 2, 2026 01:03
CarsonRoscoe and others added 16 commits May 4, 2026 10:03
…h_settlement_evm_*

BREAKING CHANGE: every facilitator-emitted batch-settlement EVM rejection token
now starts with `invalid_batch_settlement_evm_` (mirroring the
`invalid_exact_evm_*` shape already used by the exact mechanism), aligning the
SDK 1:1 with CDP Accounts API enums (`x402VerifyInvalidReason` /
`x402SettleErrorReason`). Inner `_invalid_` segments collapse into the leading
envelope so values stay short:

  batch_settlement_evm_invalid_scheme         -> invalid_batch_settlement_evm_scheme
  batch_settlement_evm_invalid_deposit_payload -> invalid_batch_settlement_evm_deposit_payload
  batch_settlement_evm_permit2_invalid_spender -> invalid_batch_settlement_evm_permit2_spender
  batch_settlement_evm_eip2612_invalid_format  -> invalid_batch_settlement_evm_eip2612_format

Resource-server-emitted abort reasons (`batch_settlement_channel_busy`,
`batch_settlement_refund_*`, `batch_settlement_cumulative_amount_mismatch`)
keep their `batch_settlement_*` sibling prefix — they are NOT facilitator
output and intentionally live in their own namespace so cdp-facilitator can
route facilitator vs server reasons separately without substring ambiguity.

Exported Go symbols (`ErrPermit2InvalidSpender`, `ErrEip2612InvalidFormat`,
etc.) are unchanged; only the wire string values move.

The `facilitator.ErrMaxClaimableTooLow` constant is now aliased from
`batched.ErrCumulativeBelowClaimed` so the corrective-recovery client check
in `client/scheme.go` and the facilitator emitter share a single source of
truth and can never drift.

Downstream consumers (notably cdp-facilitator's `MapBatchSDKReasonToCDP`)
must coordinate: drop the legacy `batch_settlement_evm_*` translation in
favor of identity passthrough, or ship a deprecation shim during the
migration window.

Tests:
  - constants_test.go pins the new `invalid_batch_settlement_evm_` prefix on
    facilitator-mirroring constants and confirms the sibling-prefix discipline
    on `ErrCumulativeAmountMismatch`.
  - facilitator/errors_test.go inventories every exported facilitator
    constant for non-empty + canonical-prefix + uniqueness, pins the exact
    19 tokens covered by the CDP wire contract, and adds
    `TestNoLegacyBatchSettlementEvmPrefix` as a guardrail against
    accidental reintroduction of the old prefix.

Verification: full Go test suite green; all e2e + example Go modules build
clean; integration tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Audit of `go/mechanisms/evm/batched/facilitator/errors.go` (56 constants)
plus the resource-server emission sites uncovered three orphan facilitator
constants and six hard-coded server abort literals that should be tracked:

Orphans removed or wired up:

  - ErrMissingEip712Domain — DELETED. Unreachable in the batch-settlement
    scheme: voucher EIP-712 hashing uses a fixed `BatchSettlementDomain`,
    not payer-supplied `requirements.extra.{name,version}` like the exact
    EIP-3009 path. The constant was carried for parity with exact but had
    no path to emit. errors.go now carries an explanatory comment so a
    future patch that re-introduces a payer-supplied domain field can add
    it back at the same time as the emitter.

  - ErrChannelNotFound — WIRED. Added `state.Balance.Sign() == 0` check in
    `verifyVoucherFields` so a non-existent or fully-drained channel emits
    a dedicated reason instead of falling through to ErrMaxClaimableTooLow.
    Mirrors TS `verifyVoucher` (voucher.ts:62) byte-for-byte.

  - ErrPermit2AllowanceRequired — WIRED. After a standard-path Permit2
    deposit simulation reverts, the new helper
    `diagnosePermit2AllowanceShortfall` reads on-chain
    `allowance(payer, Permit2)` and emits the dedicated reason when the
    allowance is below the deposit amount. Falls back to the generic
    `ErrDepositSimulationFailed` on any RPC error or sufficient allowance.
    Mirrors exact's `CheckPermit2Prerequisites` diagnosis pattern.

Server abort literals promoted to exported constants under
`batched/errors.go`'s sibling-prefix block (these are resource-server
output, NOT facilitator output, so they keep the `batch_settlement_*` and
`missing_*` namespaces — never `invalid_*`):

  - ErrChannelBusy                    = "batch_settlement_channel_busy"
  - ErrMissingChannel                 = "missing_batch_settlement_channel"
  - ErrChargeExceedsSignedCumulative  = "batch_settlement_charge_exceeds_signed_cumulative"
  - ErrRefundNoBalance                = "batch_settlement_refund_no_balance"
  - ErrRefundAmountInvalid            = "batch_settlement_refund_amount_invalid"
  - ErrRefundAmountExceedsBalance     = "batch_settlement_refund_amount_exceeds_balance"

Wire values are unchanged — both Go and TS resource servers continue
emitting the same strings. The promotion just makes the inventory
trackable and drift-resistant: server/hooks.go, client/refund.go's
non-recoverable classifier, server/hooks_test.go, and the integration
test all now reference the constants instead of hard-coding the literals.

Tests:

  - constants_test.go now asserts the sibling-prefix discipline on every
    server-emitted constant (must start `batch_settlement_*`, must NOT
    carry the `invalid_` envelope) plus the special `missing_*` envelope
    on ErrMissingChannel.
  - facilitator/errors_test.go inventory drops ErrMissingEip712Domain
    with a comment pointing at errors.go's rationale.
  - All previously hard-coded literal assertions in tests now reference
    the canonical batched.Err* constants so a future rename trips the
    tests instead of leaking through to wire consumers.

Verification: full Go test suite green; all 5 Go e2e modules and 3
example modules build clean; zero hard-coded reason literals remain in
non-test batched Go code; zero orphan constants.

Co-authored-by: Cursor <cursoragent@cursor.com>
@@ -0,0 +1,205 @@
# Batch-Settlement EVM Scheme (`go/mechanisms/evm/batched`)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename dir

Suggested change
# Batch-Settlement EVM Scheme (`go/mechanisms/evm/batched`)
# Batch-Settlement EVM Scheme (`go/mechanisms/evm/batch-settlement`)

@phdargen
Copy link
Copy Markdown
Collaborator

phdargen commented May 4, 2026

Remove examples/go/clients/batch-settlement/.!19032!batch-settlement and examples/go/servers/batch-settlement/.!19036!batch-settlement and examples/go/clients/batch-settlement/batch-settlement etc

@@ -0,0 +1,20 @@
EVM_PRIVATE_KEY=0x...
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems TS examples use .env-local, while go uses .env-example

Comment thread go/mechanisms/evm/batched/constants.go Outdated
SchemeBatched = "batch-settlement"

// BatchSettlementAddress is the deployed x402BatchSettlement contract address (CREATE2, all chains).
BatchSettlementAddress = "0x8f79473b50d67733349191d7349FE45977d44AF7"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update to latest canonical


// BuildChannelConfig constructs a ChannelConfig from payment requirements and scheme config.
func (c *BatchedEvmScheme) BuildChannelConfig(requirements types.PaymentRequirements) batched.ChannelConfig {
receiverAuthorizer := requirements.PayTo
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't fallback to payTo

@CarsonRoscoe CarsonRoscoe force-pushed the feat/batch-settlement-go-sdk branch from 3899e53 to 7351504 Compare May 4, 2026 14:52
Comment on lines +292 to +295
// SkipHandler directive: bypass downstream handler, settle inline using the
// directive body. Used for refund acknowledgements where there is no resource
// response to return.
if result.SkipHandler != nil {
Copy link
Copy Markdown
Collaborator

@phdargen phdargen May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SkipHandler does not bypass the downstream handler in gin/echo, only for nethttp

Comment thread go/server.go
Comment on lines +256 to +260
if h, ok := schemeServer.(BeforeVerifyHookProvider); ok {
if hook := h.BeforeVerifyHook(); hook != nil {
s.beforeVerifyHooks = append(s.beforeVerifyHooks, hook)
}
}
Copy link
Copy Markdown
Collaborator

@phdargen phdargen May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scheme-provided server hooks are appended into the global hook list here, so a hook from one registered scheme can run for unrelated schemes/networks. TS stores scheme hooks separately and selects only the matched scheme. In TS hooks run in this order: manual -> matched network/scheme -> declared extensions aligning with #2109.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hook order is something to double check, should it be manual app defined hooks first or last? Now thinking probably last makes more sense?

Comment on lines +292 to +293
// Use signer address as payerAuthorizer for EOA path
payerAuthorizer = c.signer.Address()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client example fails to make paid request when EVM_VOUCHER_SIGNER_PRIVATE_KEY is set.
Go signs the voucher with the voucher key, but commits the channel to the payer key as payerAuthorizer.

Suggested change
// Use signer address as payerAuthorizer for EOA path
payerAuthorizer = c.signer.Address()
if c.config.VoucherSigner != nil {
payerAuthorizer = c.config.VoucherSigner.Address()
} else {
payerAuthorizer = c.signer.Address()
}

@phdargen
Copy link
Copy Markdown
Collaborator

phdargen commented May 4, 2026

Go still misses the equivalent changes from #2109 adding extension hook adapters and making paymentRequirements and payload deepreadonly.

Go batch-settlement implementation mutates extra and payload in the hooks, what would break when the above is added. TS added explicit enrichSettlementPayload and enrichSettlementResponse scheme level hooks with additive policy checks (hooks can add but not modify)


## Client Usage

Register `BatchedEvmScheme` with an `x402Client`. The client handles deposit, voucher signing, channel-state recovery, and corrective 402 resync transparently.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want to rename the class names as well

Suggested change
Register `BatchedEvmScheme` with an `x402Client`. The client handles deposit, voucher signing, channel-state recovery, and corrective 402 resync transparently.
Register `BatchSettlementEvmScheme` with an `x402Client`. The client handles deposit, voucher signing, channel-state recovery, and corrective 402 resync transparently.

@CarsonRoscoe CarsonRoscoe deleted the branch x402-foundation:batched-settlement May 4, 2026 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

examples Changes to examples go sdk Changes to core v2 packages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants