Skip to content

feat(rpc): add optional paymentIdentifier to RpcSubmitPaymentArgsSchema for V2 parity#29

Merged
whoabuddy merged 2 commits into
mainfrom
feat/rpc-payment-identifier
Apr 23, 2026
Merged

feat(rpc): add optional paymentIdentifier to RpcSubmitPaymentArgsSchema for V2 parity#29
whoabuddy merged 2 commits into
mainfrom
feat/rpc-payment-identifier

Conversation

@whoabuddy

Copy link
Copy Markdown
Contributor

Motivation

CanonicalDomainBoundary.transportBoundaries.sharedDomain defines the semantic contract that must hold across both HTTP and RPC transports. The x402 V2 HTTP transport already has caller-controlled idempotency via the payment-identifier extension and a payment_identifier_conflict error reason. The RPC transport had no equivalent — this PR closes that gap.

Closes #28.

What's added

  1. PaymentIdentifierSchema (src/core/primitives.ts, exported from @aibtc/tx-schemas/core): shared Zod schema for caller-controlled idempotency keys. Charset [a-zA-Z0-9_-]{16,128}. Distinct from PaymentIdSchema (relay-assigned, requires pay_ prefix) — the identifier is caller-provided, not relay-generated.

  2. RpcSubmitPaymentRequestSchema.paymentIdentifier (optional field): lets RPC callers attach an idempotency key to submitPayment. Semantics mirror the HTTP extension: same identifier + same txHex reuses the existing paymentId; same identifier + different txHex is rejected with the new error code.

  3. RPC_PAYMENT_IDENTIFIER_CONFLICT added to RpcErrorCodeSchema: RPC-transport parity for the HTTP-side payment_identifier_conflict error reason. Follows existing RPC_* naming convention.

  4. CanonicalDomainBoundary.transportBoundaries.sharedDomain updated to include "paymentIdentifier idempotency", making the cross-transport contract explicit in the canonical boundary definition.

DRY improvement: HttpPaymentIdentifierExtensionSchema.info.id now uses the new shared PaymentIdentifierSchema instead of PaymentIdSchema. This corrects a subtle mismatch — extension IDs are caller-provided, so the pay_ prefix requirement was inappropriate.

Backward compatibility

Purely additive — the paymentIdentifier field is optional. Existing callers that don't set it are unaffected. No breaking changes to any existing schema.

Downstream chain

  • Relay PR #351 will consume paymentIdentifier in the RPC submitPayment handler via PaymentIdService
  • landing-page/#635 and agent-news/#624 adopt after the relay ships

Test plan

  • npm run typecheck — passes clean
  • npm run build — passes clean
  • npm test — 225 tests pass (6 new tests covering paymentIdentifier accept/reject cases and RPC_PAYMENT_IDENTIFIER_CONFLICT)
  • Backward compat: submit request without paymentIdentifier parses successfully
  • Charset validation: too-short, too-long, and disallowed-char values all rejected

…ma for V2 parity

Resolves #28.

Adds canonical x402 V2 idempotency parity to the RPC transport:

- **`PaymentIdentifierSchema`** (new, `src/core/primitives.ts`): shared caller-controlled
  idempotency key schema, `[a-zA-Z0-9_-]{16,128}`. Distinct from `PaymentIdSchema`
  (relay-assigned, `pay_` prefix). Exported from `@aibtc/tx-schemas/core`.

- **`RpcSubmitPaymentRequestSchema.paymentIdentifier`** (new, optional): lets callers
  attach a client-controlled idempotency key to `submitPayment`. Same identifier + same
  `txHex` reuses the existing `paymentId`; same identifier + different `txHex` is rejected
  with the new error code. Existing callers that omit the field are unaffected.

- **`RPC_PAYMENT_IDENTIFIER_CONFLICT`** (new error code): RPC-transport parity for the
  HTTP-side `payment_identifier_conflict` error reason. Follows the `RPC_*` naming
  convention already used in `RpcErrorCodeSchema`.

- **`CanonicalDomainBoundary.transportBoundaries.sharedDomain`** updated to include
  `"paymentIdentifier idempotency"`, reflecting that the idempotency input is now
  shared across both HTTP (payment-identifier extension) and RPC transports.

- **`HttpPaymentIdentifierExtensionSchema`** DRYed: extension `.info.id` now uses the
  new shared `PaymentIdentifierSchema` instead of the relay-assigned `PaymentIdSchema`.

Downstream: relay PR #351 will consume `paymentIdentifier` in `submitPayment`;
landing-page/#635 and agent-news/#624 adopt after.

Closes #28

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 23, 2026 08:06
- Rename RPC_PAYMENT_IDENTIFIER_CONFLICT → PAYMENT_IDENTIFIER_CONFLICT to
  match the bare naming convention of all other RPC error codes in the array
  (none carry an RPC_ prefix; the schema name already provides that context)
- Extract repeated stub txHex literal to a STUB_TX_HEX const in the
  paymentIdentifier test suite
- Trim inline comments to non-obvious WHY only

Co-Authored-By: Claude <noreply@anthropic.com>
@whoabuddy whoabuddy merged commit 159ad69 into main Apr 23, 2026
1 check passed
@whoabuddy whoabuddy deleted the feat/rpc-payment-identifier branch April 23, 2026 08:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a shared caller-controlled idempotency identifier schema and wires it into the RPC submitPayment request to match x402 V2 HTTP behavior, including an RPC-side conflict error code and an explicit note in the canonical cross-transport boundary.

Changes:

  • Introduce PaymentIdentifierSchema ([a-zA-Z0-9_-]{16,128}) as a shared primitive for caller-provided idempotency keys.
  • Add optional paymentIdentifier to RpcSubmitPaymentRequestSchema and add RPC_PAYMENT_IDENTIFIER_CONFLICT to RpcErrorCodeSchema.
  • Update HTTP payment-identifier extension to validate with PaymentIdentifierSchema and document parity in CanonicalDomainBoundary.transportBoundaries.sharedDomain.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/core/primitives.ts Adds PaymentIdentifierSchema primitive for caller-provided idempotency keys.
src/rpc/schemas.ts Extends RPC submit request with optional paymentIdentifier; adds new RPC conflict error code.
src/http/schemas.ts Switches HTTP payment-identifier extension validation to PaymentIdentifierSchema.
src/core/enums.ts Updates canonical boundary metadata to explicitly include paymentIdentifier parity across transports.
tests/rpc.test.ts Adds tests for optional paymentIdentifier validation and new RPC error code.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/http/schemas.ts
Comment on lines +49 to 55
// Uses PaymentIdentifierSchema rather than PaymentIdSchema: extension ids are caller-provided,
// not relay-assigned, so the pay_ prefix requirement was inappropriate.
export const HttpPaymentIdentifierExtensionSchema = z.object({
info: z.object({
id: PaymentIdSchema,
id: PaymentIdentifierSchema,
}),
});

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

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

Switching HttpPaymentIdentifierExtensionSchema.info.id from PaymentIdSchema to PaymentIdentifierSchema is a stricter validation change for some previously-accepted inputs (e.g., "pay_123" would have parsed before but now fails due to the 16+ length requirement). That makes the PR not purely additive as described; either (a) update the backward-compatibility notes/versioning to reflect the tightened HTTP validation, or (b) consider accepting both schemas here (e.g., union) if you need to preserve old callers while migrating.

Copilot uses AI. Check for mistakes.
Comment thread tests/rpc.test.ts
Comment on lines +151 to +153
paymentIdentifier: "pay_01JMVP9QE8XA3BDGM5",
});
expect(req.paymentIdentifier).toBe("pay_01JMVP9QE8XA3BDGM5");

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

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

The acceptance test for PaymentIdentifierSchema uses a value with a "pay_" prefix, which can unintentionally suggest that the prefix is required (even though PaymentIdentifierSchema is meant to be distinct from relay-assigned PaymentIdSchema). Consider using a clearly caller-generated identifier without the "pay_" prefix in this test to better capture the intended contract.

Suggested change
paymentIdentifier: "pay_01JMVP9QE8XA3BDGM5",
});
expect(req.paymentIdentifier).toBe("pay_01JMVP9QE8XA3BDGM5");
paymentIdentifier: "client_01JMVP9QE8XA3BDGM5",
});
expect(req.paymentIdentifier).toBe("client_01JMVP9QE8XA3BDGM5");

Copilot uses AI. Check for mistakes.
Comment thread src/rpc/schemas.ts
Comment on lines 37 to 42
"ORIGIN_CHAINING_LIMIT",
"BROADCAST_RATE_LIMITED",
"SENDER_HAND_EXPIRED",
"NONCE_OCCUPIED",
"PAYMENT_IDENTIFIER_CONFLICT",
] as const;

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

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

RPC_PAYMENT_IDENTIFIER_CONFLICT is the only entry in RPC_ERROR_CODES with an RPC_ prefix; all other RPC error codes in this enum use the unprefixed UPPER_SNAKE style (e.g., INVALID_TRANSACTION, NONCE_CONFLICT). For consistency (and to avoid downstream clients needing special-casing), consider renaming this code to match the existing pattern (or document why the prefix exception is necessary).

Copilot uses AI. Check for mistakes.
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.

rpc: add optional paymentIdentifier to RpcSubmitPaymentArgs for canonical idempotency parity with V2 facilitator

2 participants