Skip to content

Add usdc charge payment method#272

Merged
brendanjryan merged 3 commits into
tempoxyz:mainfrom
harshalbhangale-circle:add-usdc-method
Jun 16, 2026
Merged

Add usdc charge payment method#272
brendanjryan merged 3 commits into
tempoxyz:mainfrom
harshalbhangale-circle:add-usdc-method

Conversation

@harshalbhangale-circle

@harshalbhangale-circle harshalbhangale-circle commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds a usdc charge method for Payment Auth.

The method gives merchants a unified way to advertise USDC acceptance across supported chains and settlement paths. Clients still receive the concrete details they need for the selected path, including how to pay, verify, apply replay protection, and prove settlement.

In this draft, each supported payment path is modeled as a profile. The v00 profiles are direct EVM USDC, direct Solana USDC, Stacks USDCx, and Gateway Transfer.

Motivation

USDC is available across multiple chains and settlement paths. Merchants may want to integrate at the asset level while still choosing the chains and paths they support.

The usdc method provides that common layer. A merchant can advertise usdc once and indicate the supported profiles. A client with EVM support can pay through the EVM profile. A client with Solana support can pay through the Solana profile. A client using Gateway can fund from an advertised source network while the merchant settles on its chosen destination network.

This keeps the merchant integration centered on USDC while giving clients the concrete path details they need for payment and verification.

What This Adds

This draft defines the usdc charge method and four v00 profiles:

  1. Direct EVM USDC charges.
  2. Direct Solana USDC charges.
  3. Stacks USDCx charges.
  4. Gateway Transfer charges, where the merchant chooses the destination network and the payer funds from an advertised Gateway source network.

It also defines shared USDC behavior for asset identity, profile selection, replay protection, token controls, and receipts.

Notes

USDC is a common payment asset across multiple supported environments. Giving it a method-level home makes discovery and integration clearer for merchants and clients.

Merchants get one method for USDC acceptance. Clients get the profile details needed for the path they support. Servers apply common USDC request and receipt rules, then use profile-specific verification for settlement.

Gateway Transfer extends the same model to cross-chain charges. A seller can choose where it wants to receive USDC, while the payer funds from another supported Gateway network.

This keeps the public method simple: usdc for the asset, profiles for the payment path.

Verification

  • make lint
  • make check

AI Assistance Disclosure

This draft was prepared and reviewed with significant AI assistance. I reviewed the generated content for technical correctness, RFC-style formatting, and the contribution requirements in CONTRIBUTING.md.


`methodDetails.profile` selects the active `usdc` profile. Exactly one of
`methodDetails.evm`, `methodDetails.solana`, `methodDetails.stacks`, or
`methodDetails.gateway` MUST be present, and it MUST match

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.

what does this mean? must match methoddetails.profile?

won't the field always match the field?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that wording was awkward. The intent was to say that methodDetails.type is the discriminator, and exactly one matching profile object should be present. I updated this to use type instead of profile


| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `profile` | string | REQUIRED | One of `evm`, `solana`, `stacks`, or `gateway`. |

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.

not sure i love the name "profile" -- did we consider others like

  • kind
  • type

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

changed it to type

MUST reject credentials whose `tokenProgram` is not:

```text
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

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.

👍

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `acceptedSources` | array | REQUIRED | CAIP-2 Gateway source networks the payer may use for this charge. |
| `destinationNetwork` | string | REQUIRED | CAIP-2 destination network where the merchant wants to receive USDC. |

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.

does CAIP-2 cover networks like solana too?

if so, why not use CAIP-2 globally for all names?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

CAIP-2 does cover Solana. I kept CAIP-2 for Gateway source and destination network fields because Gateway can route across chain families. I did not switch the direct EVM and Solana profiles to CAIP-2 globally because those profiles inherit the identifier formats from the existing EVM and Solana PaymentAuth specs. But I dont have a strong preference

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.

I see -- this makes sense. i'm ok keeping with this

| `destinationNetwork` | string | REQUIRED | CAIP-2 destination network where the merchant wants to receive USDC. |
| `maxFee` | string | REQUIRED | Absolute upper bound on the signed Gateway authorization fee cap, in USDC base units. |
| `maxFeeBps` | number | OPTIONAL | Additional ratio cap. When present, `authorizationFeeCap * 10000 <= request.amount * maxFeeBps`. |
| `credentialTypes` | array | OPTIONAL | If present, MUST contain only `gateway-transfer` in v00. If absent, `gateway-transfer` is implied. |

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.

since this is aleady under gateway, can this just be transfer ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, updated


```text
{
"type": "challenge-binding-v00",

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.

why do we need to do this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The Solana transaction signature proves that the payer authorized the transaction bytes, but it does not itself sign the PaymentAuth challenge. The detached binding adds a same-key signature over the exact challenge, source account, and transaction bytes digest. That gives the profile challenge-bound payer authorization similar in spirit to EVM, while keeping the challenge id offchain.

The tradeoff is extra client and SDK complexity. If we want v00 to stay closer to the existing Solana pull-mode model, I’m fine removing the detached binding for Solana and using the base verification semantics. For Stacks, where there is no base method to inherit, we can define the same server-broadcast, verify-terms, consume-transaction-id model without the second signature.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Decided to drop it for v00. The Solana profile now just inherits base pull-mode verification from solana-charge, no detached binding.

`payload.challengeBinding.signature` MUST be a base64-encoded 65-byte
recoverable secp256k1 signature in `r || s || v` order, with `v` in
`{0x00, 0x01}`. This document defines the SIP-018 domain tuple for the
challenge-binding message. The `USDCPaymentAuth` name is a

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.

similar to above -- why is this strictly needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed for v00. Stacks now defines plain server-broadcast verification: check the SIP-010 transfer, its post-condition, and the origin signature, then bind to the challenge through challenge consumption and transaction-id replay. No second SIP-018 signature

| `reference` | string | REQUIRED | Transaction hash, signature, or transaction ID. |
| `referenceType` | string | REQUIRED | `tx_hash`, `tx_signature`, or `tx_id`. |
| `status` | string | REQUIRED | `success` only after the selected profile has completed settlement. |
| `timestamp` | string | REQUIRED | RFC3339 settlement time. |

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.

why do we need to add all of these fields to the receipt? are all of these strictly necessary?

this is a large addition to the existing types and would make it harder to write an SDK

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That makes sense. Reduced the receipt shape substantially. This should be easier for SDKs while still preserving enough settlement context for the selected USDC profile.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

Spec Preview

Spec Changed Artifacts
draft-card-charge-00 Yes HTML · TXT · XML · PDF
draft-evm-charge-00 Yes HTML · TXT · XML · PDF
draft-hedera-charge-00 Yes HTML · TXT · XML · PDF
draft-httpauth-payment-00 Yes HTML · TXT · XML · PDF
draft-lightning-charge-00 Yes HTML · TXT · XML · PDF
draft-lightning-session-00 Yes HTML · TXT · XML · PDF
draft-payment-discovery-00 Yes HTML · TXT · XML · PDF
draft-payment-intent-charge-00 Yes HTML · TXT · XML · PDF
draft-payment-transport-mcp-00 Yes HTML · TXT · XML · PDF
draft-solana-charge-00 Yes HTML · TXT · XML · PDF
draft-stellar-charge-00 Yes HTML · TXT · XML · PDF
draft-stripe-charge-00 Yes HTML · TXT · XML · PDF
draft-tempo-charge-00 Yes HTML · TXT · XML · PDF
draft-tempo-session-00 Yes HTML · TXT · XML · PDF
draft-usdc-charge-00 New HTML · TXT · XML · PDF

Browse preview release assets

@brendanjryan brendanjryan left a comment

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.

Left some final reviews! Overall i'm not fully convinced for the need for CAIP prefixes as all of the methods are scoped to a specific chain as is.

In other methods (solana, evm etc..) we just use the chaindId field to discriminate, as the prefix is implicit in the method (e.g. all solana chain ids are implicitly under solana)

| --- | --- | --- | --- |
| `challenge` | object | REQUIRED | Echo of the server challenge. |
| `payload` | object | REQUIRED | Solana payment payload. |
| `source` | string | REQUIRED | CAIP-10 account ID using `solana:<genesis-hash-prefix>:<pubkey>`. |

@brendanjryan brendanjryan Jun 10, 2026

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.

source encoding contradicts core/evm/solana.

This draft requires bare CAIP-10 for source across all profiles (here solana:<genesis>:<pubkey>, and eip155:5042002:0x… in the EVM example at line 1121). But:

  • core recommends DID format (source = "Payer identifier (RECOMMENDED: DID format per [W3C-DID])")
  • evm-charge §5.1: source SHOULD be did:pkh:eip155:…
  • solana-charge: source MAY be a base58 pubkey or a DID

So the EVM example "source":"eip155:…" directly contradicts evm-charge's did:pkh:eip155:…. This draft also makes source REQUIRED, whereas all three base specs make it OPTIONAL.

Please either adopt the DID/did:pkh convention used by the rest of the family, or get core/evm/solana updated to bless CAIP-10 — and reconcile the required-vs-optional difference.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the catch. Fixed it. Reconciled per profile, aligning the direct profiles with their base specs:

  • EVM: source is now OPTIONAL with did:pkh:eip155:… RECOMMENDED, matching evm-charge §5.1. The line-1121 example now reads did:pkh:eip155:5042002:0x….
  • Solana: source OPTIONAL, may be a base58 pubkey or a DID (did:pkh:solana:… recommended), matching solana-charge. Example updated to did:pkh:….
  • Gateway Transfer: source stays REQUIRED and bare CAIP-10. It's the source depositor account, and Gateway needs it named to build and verify the burn/transfer authorization, so it can't
    be optional here. There's no base method for it to contradict.
  • Stacks: source stays REQUIRED and CAIP-10 (stacks::). No base MPP method to inherit a DID convention from, and the origin signature recovers to this principal
    during verification.

`requestHash` is `keccak256` of the UTF-8 bytes of the exact
JCS-canonicalized request JSON before base64url encoding.

This is stricter than the base EVM charge profile. The base EVM profile

@brendanjryan brendanjryan Jun 10, 2026

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.

EVM authorization nonce derivation conflicts with evm-charge

evm-charge §5.3.1 defines nonce = keccak256(abi.encodePacked(challenge.id, challenge.realm)). This draft (line 516) defines nonce = keccak256(JCS({id, method, realm, intent, requestHash})). These are not interchangeable.

The text just above says the EVM profile "inherit[s] {{I-D.evm-charge}} authorization payloads," but a generic evm-charge verifier will compute a different expected nonce and reject these credentials. The stricter binding is reasonable, but please reword to state explicitly that the nonce binding is overridden (not inherited) and that usdc EVM credentials are not interchangeable with method="evm".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reworded. It now says the nonce derivation overrides (does not inherit) the base evm-charge derivation


## Method Details

`methodDetails.type` selects the active `usdc` profile. Its value MUST

@brendanjryan brendanjryan Jun 10, 2026

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.

methodDetails shape diverges from evm-charge/solana-charge

Base evm-charge and solana-charge place method fields flat in methodDetails (methodDetails.chainId, methodDetails.network). This draft introduces a methodDetails.type discriminator plus a nested profile object (methodDetails.evm.chainId).

I think this is a sensible design for a method multiplexing four profiles, but it means the EVM/Solana request envelope is not literally inherited — a base-spec methodDetails parser won't work here.

Please call this divergence out explicitly so implementers don't assume drop-in reuse.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. There's now a note right after the methodDetails table

| `reference` | string | Final settlement reference. |
| `status` | string | MUST be `success` only after the selected profile has completed settlement. |
| `timestamp` | string | RFC3339 settlement time. |
| `network` | string | Settlement network identifier. |

@brendanjryan brendanjryan Jun 10, 2026

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.

Receipt settlement-locator inconsistent with sibling specs

This receipt uses network (CAIP-2 string) for every profile. But evm-charge's receipt uses chainId (number), and solana-charge's receipt has no network field at all. Cross-method receipt consumers will see a different locator field depending on method.

Consider either keeping chainId (number) for the EVM profile to match evm-charge, or explicitly documenting the deliberate switch to a unified CAIP-2 network field and noting it differs from the base EVM/Solana receipts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Kept the unified field but documented it. network is now typed CAIP-2 in the table, with a note that it's a deliberate, method-wide settlement locator that differs from the base EVM receipt (numeric chainId) and the base Solana receipt (no network field). A usdc receipt is asset-level, so a consumer reads one locator regardless of profile, and a Gateway destination may not be EVM, so a numeric chainId wouldn't work there anyway.

@brendanjryan brendanjryan left a comment

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.

LGTM -- thanks for all of the revisions on this!

@brendanjryan brendanjryan merged commit f2aa3b4 into tempoxyz:main Jun 16, 2026
2 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.

2 participants