From 87549703e65c56cc691e39f9d2676f58115837fc Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:40:17 +0900 Subject: [PATCH 01/19] specs(exact): propose TON exact scheme for x402 v2 (spec-only) Adds scheme_exact_ton.md defining the exact payment scheme for TON blockchain. Uses W5 wallet internal_signed messages for gasless Jetton (TEP-74) transfers. Updates scheme_exact.md with TON-specific validation rules. --- specs/schemes/exact/scheme_exact.md | 9 +- specs/schemes/exact/scheme_exact_ton.md | 253 ++++++++++++++++++++++++ 2 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 specs/schemes/exact/scheme_exact_ton.md diff --git a/specs/schemes/exact/scheme_exact.md b/specs/schemes/exact/scheme_exact.md index 96268e1f39..d85a2ba322 100644 --- a/specs/schemes/exact/scheme_exact.md +++ b/specs/schemes/exact/scheme_exact.md @@ -30,4 +30,11 @@ While implementation details vary by network, facilitators MUST enforce security - Transfer correctness: `to` MUST equal `payTo` and `amount` MUST equal `requirements.amount` exactly. - Simulation verification: MUST emit events showing only the expected balance changes (recipient increase, payer decrease) for `requirements.amount`—no other balance changes allowed. -Network-specific rules are in per-network documents: `scheme_exact_svm.md` (Solana), `scheme_exact_stellar.md` (Stellar), `scheme_exact_evm.md` (EVM), `scheme_exact_sui.md` (SUI). +### TON + +- Relay safety: the relay/facilitator address MUST NOT appear as the source of any Jetton transfer or as the `walletAddress` (payer). +- Transfer correctness: `jetton_transfer` destination MUST equal `payTo` (after Jetton wallet resolution) and `amount` MUST be `>=` `requirements.amount`. +- Seqno validation: `seqno` MUST match the wallet's current on-chain seqno for replay protection. +- Simulation verification: MUST confirm expected balance changes (recipient increase, payer decrease) before broadcast. + +Network-specific rules are in per-network documents: `scheme_exact_svm.md` (Solana), `scheme_exact_stellar.md` (Stellar), `scheme_exact_evm.md` (EVM), `scheme_exact_sui.md` (SUI), `scheme_exact_ton.md` (TON). diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md new file mode 100644 index 0000000000..733edc0714 --- /dev/null +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -0,0 +1,253 @@ +# Scheme: `exact` on `TON` + +## Versions supported + +- ❌ `v1` +- ✅ `v2` + +## Supported Networks + +This spec uses [CAIP-2](https://namespaces.chainagnostic.org/tvm/caip2) identifiers from the TVM namespace: + +- `tvm:-239` — TON mainnet +- `tvm:-3` — TON testnet + +> [!NOTE] +> **Scope:** This spec covers [TEP-74]-compliant Jetton transfers using **W5+ wallets** (v5r1 and later) only. Earlier wallet versions (v3, v4) do not support `internal_signed` messages required for gasless transactions. + +## Summary + +The `exact` scheme on TON transfers a specific amount of a [TEP-74] Jetton from the client to the resource server using a W5 wallet `internal_signed` message. + +The facilitator sponsors gas by wrapping the client-signed message in an internal TON message. The client controls payment intent (asset, recipient, amount) through Ed25519 signature. The facilitator cannot modify the destination or amount. + +## Protocol Flow + +1. Client requests a protected resource. +2. Resource server returns a payment-required signal with `PAYMENT-REQUIRED` and `PaymentRequired` data. +3. `accepts[].extra.relayAddress` communicates the gasless relay address for excess funds. +4. Client resolves their Jetton wallet address via `get_wallet_address` on the Jetton master. +5. Client builds a `jetton_transfer` body ([TEP-74] opcode `0xf8a7ea5`). +6. Client wraps in a W5 `internal_signed` message with `seqno` + `timeout`. +7. Client wraps in an external message BOC (with `stateInit` if `seqno == 0`). +8. Client sends a second request with `PAYMENT-SIGNATURE`, containing a base64-encoded `PaymentPayload`. +9. Resource server forwards payload and requirements to facilitator `/verify`. +10. Facilitator deserializes BOC, verifies signature, intent, and amounts. + - NOTE: `/verify` is optional and intended for pre-flight checks only. `/settle` MUST perform full verification independently and MUST NOT assume prior verification. +11. Resource server fulfills work after successful verification. +12. Resource server calls facilitator `/settle`. +13. Facilitator broadcasts signed BOC via gasless relay or direct submission to validators. +14. Resource server returns the final response including `PAYMENT-RESPONSE`. + +## `PaymentRequirements` for `exact` + +In addition to standard x402 fields, TON `exact` uses `extra` fields: + +```json +{ + "scheme": "exact", + "network": "tvm:-239", + "amount": "10000", + "asset": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", + "payTo": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", + "maxTimeoutSeconds": 300, + "extra": { + "relayAddress": "0:7ae5056c3fd9406f9bbbe7c7089cd4c40801d9075486cbedb7ce12df119eacf1", + "assetDecimals": 6, + "assetSymbol": "USDT" + } +} +``` + +**Field Definitions:** + +- `asset`: [TEP-74] Jetton master contract address (raw format `workchain:hex`). +- `payTo`: Recipient TON address (raw format). +- `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). +- `extra.relayAddress`: (Optional) Gasless relay address that receives excess TON, reducing fees for the client. When present, the client should set `response_destination` in `jetton_transfer` to this address. When absent, the client handles excess routing itself. +- `extra.assetDecimals`: Token decimal places for display purposes. +- `extra.assetSymbol`: Human-readable token symbol. + +## PaymentPayload `payload` Field + +The `payload` field contains the signed external message BOC and wallet metadata: + +```json +{ + "signedBoc": "BASE64_EXTERNAL_MESSAGE_BOC", + "walletPublicKey": "HEX_ED25519_PUBLIC_KEY", + "walletAddress": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f", + "seqno": 0, + "validUntil": 1772689900 +} +``` + +Full `PaymentPayload` object: + +```json +{ + "x402Version": 2, + "resource": { + "url": "https://api.example.com/wallet-analytics", + "description": "TON wallet analytics", + "mimeType": "application/json" + }, + "accepted": { + "scheme": "exact", + "network": "tvm:-239", + "amount": "10000", + "asset": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", + "payTo": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", + "maxTimeoutSeconds": 300, + "extra": { + "relayAddress": "0:7ae5056c3fd9406f9bbbe7c7089cd4c40801d9075486cbedb7ce12df119eacf1", + "assetDecimals": 6, + "assetSymbol": "USDT" + } + }, + "payload": { + "signedBoc": "te6cckEBAgEAkwABnYgBFpKiX...", + "walletPublicKey": "14f77792ea084b4defa9bf5e99335682dd556b8ddf1943dca052ca56276136a8", + "walletAddress": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f", + "seqno": 3, + "validUntil": 1772689900 + } +} +``` + +**Field Definitions:** + +- `signedBoc`: Base64-encoded external message containing a W5 signed transfer. +- `walletPublicKey`: Ed25519 public key in hex, used by gasless relay for submission. +- `walletAddress`: Sender W5 wallet address in raw format. +- `seqno`: Current wallet sequence number (replay protection). +- `validUntil`: Unix timestamp after which the signed message expires. + +## `SettlementResponse` + +```json +{ + "success": true, + "transaction": "ba96f62d4ea651a21da4282809f2541ea42481ca35018129f29b406ef3fe36c0", + "network": "tvm:-239", + "payer": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f" +} +``` + +- `transaction`: Transaction hash (64-character hex string). +- `payer`: The address of the client who signed the payment (not the facilitator/relay). + +## Facilitator Verification Rules (MUST) + +A facilitator verifying `exact` on TON MUST enforce all checks below before settlement. + +### 1. Protocol and requirement consistency + +- `x402Version` MUST be `2`. +- `payload.accepted.scheme` and `requirements.scheme` MUST both equal `"exact"`. +- `payload.accepted.network` MUST equal `requirements.network`. +- `payload.accepted.asset` MUST equal `requirements.asset`. +- `payload.accepted.payTo` MUST equal `requirements.payTo`. +- `payload.accepted.amount` MUST be `>=` `requirements.amount`. + +### 2. Signed message validity + +- `payload.signedBoc` MUST decode as a valid TON external message. +- The message body MUST contain a valid W5 (v5r1+) signed transfer with `authType: internal`. +- The Ed25519 signature MUST verify against `payload.walletPublicKey`. +- `payload.validUntil` MUST be in the future but within `maxTimeoutSeconds` of the current time. + +### 3. Payment intent integrity + +- The W5 message MUST contain outgoing internal messages. +- At least one internal message MUST be a `jetton_transfer` (opcode `0xf8a7ea5`). +- The `jetton_transfer` destination (after Jetton wallet resolution) MUST match `requirements.payTo`. +- The transfer amount MUST be `>=` `requirements.amount`. +- The Jetton master contract MUST match `requirements.asset`. + +### 4. Replay and anti-abuse checks + +- `payload.seqno` MUST match the wallet's current on-chain seqno. +- Duplicate `signedBoc` submissions MUST be rejected. +- The W5 message MUST NOT contain additional unrelated actions beyond the payment transfer and relay commission. + +### 5. Relay sponsorship safety + +> [!IMPORTANT] +> These checks prevent the relay/facilitator from being tricked into transferring their own funds or sponsoring unintended actions. + +- The facilitator/relay account MUST NOT appear as the source of the Jetton transfer. +- The facilitator MUST NOT be the payer (`walletAddress`) for the delegated transfer. +- The facilitator address MUST NOT appear as destination in any `jetton_transfer` within the W5 message (except for the relay commission transfer to `extra.relayAddress`). +- Gas costs MUST be bounded by facilitator policy to prevent sponsorship drain. + +### 6. Pre-settlement simulation + +- Facilitator SHOULD simulate message execution before broadcast. +- Settlement MUST fail if: insufficient Jetton balance, expired message, or invalid seqno. +- Simulation MUST confirm the expected balance changes: recipient receives `>= requirements.amount`, payer balance decreases accordingly. + +## Settlement Logic + +1. Re-run all verification checks (do not trust prior `/verify` result). +2. Submit `signedBoc` via gasless relay or direct broadcast: + - **Sponsored (gasless):** `POST /v2/gasless/send` with `{ wallet_public_key, boc }` to a relay service. The relay wraps the signed message in an internal message carrying TON for gas. + - **Non-sponsored (direct):** Broadcast the external message directly to TON validators. The client must have sufficient TON balance for gas fees in this mode. +3. Wait for transaction confirmation (typically < 5 seconds on TON). +4. Return x402 `SettlementResponse` with `success`, `transaction`, `network`, and `payer`. + +## Appendix + +### W5 Wallet and Gasless Architecture + +The W5 wallet contract (v5, deployed since Aug 2024) introduced `internal_signed` messages — the key primitive for gasless transfers on TON: + +1. **User signs** a message containing outgoing transfers (e.g., USDT to recipient + small USDT fee to relay). +2. **Signed message is sent off-chain** via HTTPS to a gasless relay service. +3. **Relay wraps it** in an internal message (which carries TON for gas) and submits to the user's W5 wallet contract. +4. **W5 contract verifies** the Ed25519 signature and executes the transfers. +5. **Relay is compensated** via USDT from a commission transfer included in the signed batch. + +This is architecturally equivalent to x402's facilitator model: + +| x402 Concept | TON Equivalent | +|---|---| +| Facilitator | Gasless relay | +| EIP-3009 `transferWithAuthorization` | W5 `internal_signed` message | +| Gas sponsorship | Relay wraps in internal message carrying TON | + +The relay is **optional**. Any entity that can submit an internal message to a W5 contract can act as a relay. Reference implementations include the [TONAPI Gasless API][TONAPI]. + +### TON Address Formats + +- **Raw**: `workchain:hex` (e.g., `0:b113a994...`) — used in this protocol. +- **Friendly non-bounceable**: `UQ...` — used in user-facing UIs. +- **Friendly bounceable**: `EQ...` — used for smart contracts. + +Implementations MUST use raw format in protocol messages and MAY display friendly format in UIs. + +### TEP-74 Jetton Standard + +TON uses the [TEP-74 Jetton standard][TEP-74] for fungible tokens: + +- Transfer opcode: `0xf8a7ea5` (`jetton_transfer`). +- Each holder has a separate Jetton wallet contract. +- The Jetton master contract resolves wallet addresses via `get_wallet_address` getter. + +### Default Assets + +| Network | Asset | Symbol | Decimals | Address | +|---|---|---|---|---| +| `tvm:-239` | USDT | USD₮ | 6 | `0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe` | + +### References + +- [x402 v2 core specification](../../x402-specification-v2.md) +- [TEP-74 Jetton Standard][TEP-74] +- [W5 Wallet Contract](https://github.com/ton-blockchain/wallet-contract-v5) +- [TONAPI Gasless API][TONAPI] +- [TVM CAIP-2 Namespace](https://namespaces.chainagnostic.org/tvm/caip2) +- [Working Demo](https://github.com/ohld/x402-ton-poc) + +[TEP-74]: https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md +[TONAPI]: https://docs.tonconsole.com/tonapi/rest-api/gasless From 9f6683f5746904c1b21125675c83d38e58c672e4 Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:20:59 +0900 Subject: [PATCH 02/19] =?UTF-8?q?specs(exact/ton):=20address=20review=20fe?= =?UTF-8?q?edback=20=E2=80=94=20exact=20amounts,=20commission=20bounds,=20?= =?UTF-8?q?stateInit=20verification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change amount verification from >= to == (exact match), aligning with all other chain specs (SVM, Stellar, EVM). The relay commission is a separate jetton_transfer in the W5 batch, not bundled into the payment. - Add extra.maxRelayCommission field to PaymentRequirements, allowing resource servers to cap the relay commission amount. - Add stateInit verification rule: when seqno == 0, facilitator must verify contract code against a known W5 wallet code hash allowlist. - Tighten action count: W5 message must contain exactly the payment transfer and optionally a relay commission transfer, nothing else. --- specs/schemes/exact/scheme_exact.md | 4 +++- specs/schemes/exact/scheme_exact_ton.md | 15 ++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/specs/schemes/exact/scheme_exact.md b/specs/schemes/exact/scheme_exact.md index d85a2ba322..275be05a95 100644 --- a/specs/schemes/exact/scheme_exact.md +++ b/specs/schemes/exact/scheme_exact.md @@ -33,7 +33,9 @@ While implementation details vary by network, facilitators MUST enforce security ### TON - Relay safety: the relay/facilitator address MUST NOT appear as the source of any Jetton transfer or as the `walletAddress` (payer). -- Transfer correctness: `jetton_transfer` destination MUST equal `payTo` (after Jetton wallet resolution) and `amount` MUST be `>=` `requirements.amount`. +- Transfer correctness: `jetton_transfer` destination MUST equal `payTo` (after Jetton wallet resolution) and transfer amount MUST equal `requirements.amount` exactly. +- Commission bounds: if `extra.maxRelayCommission` is set, the relay commission transfer MUST NOT exceed it. +- Wallet verification: if `stateInit` is present (`seqno == 0`), the contract code MUST match a known W5 wallet code hash. - Seqno validation: `seqno` MUST match the wallet's current on-chain seqno for replay protection. - Simulation verification: MUST confirm expected balance changes (recipient increase, payer decrease) before broadcast. diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index 733edc0714..145da7e82f 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -53,6 +53,7 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: "maxTimeoutSeconds": 300, "extra": { "relayAddress": "0:7ae5056c3fd9406f9bbbe7c7089cd4c40801d9075486cbedb7ce12df119eacf1", + "maxRelayCommission": "50000", "assetDecimals": 6, "assetSymbol": "USDT" } @@ -64,7 +65,8 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: - `asset`: [TEP-74] Jetton master contract address (raw format `workchain:hex`). - `payTo`: Recipient TON address (raw format). - `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). -- `extra.relayAddress`: (Optional) Gasless relay address that receives excess TON, reducing fees for the client. When present, the client should set `response_destination` in `jetton_transfer` to this address. When absent, the client handles excess routing itself. +- `extra.relayAddress`: (Optional) Gasless relay address that receives the relay commission. When present, the W5 batch includes a separate `jetton_transfer` to this address as commission for gas sponsorship. When absent, the client handles gas fees directly. +- `extra.maxRelayCommission`: (Optional) Maximum relay commission in atomic token units. When present, facilitator MUST reject W5 batches where the commission transfer exceeds this amount. When absent, the facilitator applies its own commission policy. - `extra.assetDecimals`: Token decimal places for display purposes. - `extra.assetSymbol`: Human-readable token symbol. @@ -101,6 +103,7 @@ Full `PaymentPayload` object: "maxTimeoutSeconds": 300, "extra": { "relayAddress": "0:7ae5056c3fd9406f9bbbe7c7089cd4c40801d9075486cbedb7ce12df119eacf1", + "maxRelayCommission": "50000", "assetDecimals": 6, "assetSymbol": "USDT" } @@ -148,7 +151,7 @@ A facilitator verifying `exact` on TON MUST enforce all checks below before sett - `payload.accepted.network` MUST equal `requirements.network`. - `payload.accepted.asset` MUST equal `requirements.asset`. - `payload.accepted.payTo` MUST equal `requirements.payTo`. -- `payload.accepted.amount` MUST be `>=` `requirements.amount`. +- `payload.accepted.amount` MUST equal `requirements.amount` exactly. ### 2. Signed message validity @@ -162,14 +165,16 @@ A facilitator verifying `exact` on TON MUST enforce all checks below before sett - The W5 message MUST contain outgoing internal messages. - At least one internal message MUST be a `jetton_transfer` (opcode `0xf8a7ea5`). - The `jetton_transfer` destination (after Jetton wallet resolution) MUST match `requirements.payTo`. -- The transfer amount MUST be `>=` `requirements.amount`. +- The transfer amount MUST equal `requirements.amount` exactly. - The Jetton master contract MUST match `requirements.asset`. ### 4. Replay and anti-abuse checks - `payload.seqno` MUST match the wallet's current on-chain seqno. - Duplicate `signedBoc` submissions MUST be rejected. -- The W5 message MUST NOT contain additional unrelated actions beyond the payment transfer and relay commission. +- The W5 message MUST contain exactly two actions: the payment `jetton_transfer` and (optionally) a relay commission `jetton_transfer`. Any additional actions MUST cause rejection. +- If `extra.maxRelayCommission` is present, the relay commission transfer amount MUST NOT exceed `extra.maxRelayCommission`. If absent, the facilitator SHOULD apply its own commission policy. +- If `payload.seqno == 0` and `stateInit` is present, the facilitator MUST verify that the contract code in `stateInit` matches a known W5 wallet contract. Facilitators SHOULD maintain an allowlist of accepted wallet code hashes and reject unknown contracts. ### 5. Relay sponsorship safety @@ -185,7 +190,7 @@ A facilitator verifying `exact` on TON MUST enforce all checks below before sett - Facilitator SHOULD simulate message execution before broadcast. - Settlement MUST fail if: insufficient Jetton balance, expired message, or invalid seqno. -- Simulation MUST confirm the expected balance changes: recipient receives `>= requirements.amount`, payer balance decreases accordingly. +- Simulation MUST confirm the expected balance changes: recipient receives exactly `requirements.amount`, payer balance decreases accordingly. ## Settlement Logic From 655fce5427ed899c4312928d3333b4ccf917802b Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:43:04 +0900 Subject: [PATCH 03/19] =?UTF-8?q?specs(exact/ton):=20rewrite=20to=20self-r?= =?UTF-8?q?elay=20architecture=20=E2=80=94=20address=20all=20review=20comm?= =?UTF-8?q?ents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/schemes/exact/scheme_exact.md | 11 +- specs/schemes/exact/scheme_exact_ton.md | 277 ++++++++++++++++-------- 2 files changed, 189 insertions(+), 99 deletions(-) diff --git a/specs/schemes/exact/scheme_exact.md b/specs/schemes/exact/scheme_exact.md index 275be05a95..03827fc364 100644 --- a/specs/schemes/exact/scheme_exact.md +++ b/specs/schemes/exact/scheme_exact.md @@ -32,11 +32,10 @@ While implementation details vary by network, facilitators MUST enforce security ### TON -- Relay safety: the relay/facilitator address MUST NOT appear as the source of any Jetton transfer or as the `walletAddress` (payer). -- Transfer correctness: `jetton_transfer` destination MUST equal `payTo` (after Jetton wallet resolution) and transfer amount MUST equal `requirements.amount` exactly. -- Commission bounds: if `extra.maxRelayCommission` is set, the relay commission transfer MUST NOT exceed it. -- Wallet verification: if `stateInit` is present (`seqno == 0`), the contract code MUST match a known W5 wallet code hash. -- Seqno validation: `seqno` MUST match the wallet's current on-chain seqno for replay protection. -- Simulation verification: MUST confirm expected balance changes (recipient increase, payer decrease) before broadcast. +- Self-relay safety: the facilitator address MUST NOT appear as the source of any Jetton transfer or as the sender wallet address. +- Transfer correctness: exactly 1 `jetton_transfer` — destination MUST equal `payTo` (after Jetton wallet resolution) and transfer amount MUST equal `requirements.amount` exactly. +- Signature validity: Ed25519 signature MUST verify against `walletPublicKey` for both `internal_signed` (0x73696e74) and `external_signed` (0x7369676e) opcodes. +- Replay protection: seqno MUST NOT be stale; duplicate `settlementBoc` submissions rejected via BoC hash dedup. +- Simulation verification: SHOULD simulate via emulation before broadcast to confirm expected balance changes. Network-specific rules are in per-network documents: `scheme_exact_svm.md` (Solana), `scheme_exact_stellar.md` (Stellar), `scheme_exact_evm.md` (EVM), `scheme_exact_sui.md` (SUI), `scheme_exact_ton.md` (TON). diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index 145da7e82f..6ab38579f0 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -17,27 +17,30 @@ This spec uses [CAIP-2](https://namespaces.chainagnostic.org/tvm/caip2) identifi ## Summary -The `exact` scheme on TON transfers a specific amount of a [TEP-74] Jetton from the client to the resource server using a W5 wallet `internal_signed` message. +The `exact` scheme on TON transfers a specific amount of a [TEP-74] Jetton from the client to the resource server using a W5 wallet signed message. -The facilitator sponsors gas by wrapping the client-signed message in an internal TON message. The client controls payment intent (asset, recipient, amount) through Ed25519 signature. The facilitator cannot modify the destination or amount. +The facilitator IS the relay. It sponsors gas (~0.013 TON per transaction) by wrapping the client-signed message in an internal TON message from its own funded wallet. The client makes zero blockchain calls — it calls the facilitator's `/prepare` endpoint to get signing data, signs locally, and sends the result. The facilitator cannot modify the destination or amount; the client controls payment intent through Ed25519 signature. + +There is no relay commission. The facilitator absorbs gas costs as the cost of operating the payment network, analogous to how EVM facilitators pay gas for `transferWithAuthorization`. ## Protocol Flow -1. Client requests a protected resource. -2. Resource server returns a payment-required signal with `PAYMENT-REQUIRED` and `PaymentRequired` data. -3. `accepts[].extra.relayAddress` communicates the gasless relay address for excess funds. -4. Client resolves their Jetton wallet address via `get_wallet_address` on the Jetton master. -5. Client builds a `jetton_transfer` body ([TEP-74] opcode `0xf8a7ea5`). -6. Client wraps in a W5 `internal_signed` message with `seqno` + `timeout`. -7. Client wraps in an external message BOC (with `stateInit` if `seqno == 0`). -8. Client sends a second request with `PAYMENT-SIGNATURE`, containing a base64-encoded `PaymentPayload`. -9. Resource server forwards payload and requirements to facilitator `/verify`. -10. Facilitator deserializes BOC, verifies signature, intent, and amounts. - - NOTE: `/verify` is optional and intended for pre-flight checks only. `/settle` MUST perform full verification independently and MUST NOT assume prior verification. -11. Resource server fulfills work after successful verification. -12. Resource server calls facilitator `/settle`. -13. Facilitator broadcasts signed BOC via gasless relay or direct submission to validators. -14. Resource server returns the final response including `PAYMENT-RESPONSE`. +1. **Client** requests a protected resource from the **Resource Server**. +2. **Resource Server** responds with HTTP 402 and `PaymentRequired` data. The `accepts` array includes a TON payment option with `facilitatorUrl`. +3. **Client** calls the **Facilitator's** `/prepare` endpoint with `{ from, to, tokenMaster, amount }`. This resolves the client's Jetton wallet, fetches the current seqno, and returns signing data (seqno, validUntil, walletId, messages array). +4. **Client** constructs a W5 `internal_signed` message containing the Jetton transfer from the `/prepare` response. +5. **Client** signs the message with their Ed25519 private key. +6. **Client** wraps the signed body in an external message BOC (with `stateInit` if `seqno == 0`) and base64-encodes it. +7. **Client** sends a second request to the **Resource Server** with the `PaymentPayload` in the `X-PAYMENT` header. +8. **Resource Server** forwards the payload and requirements to the **Facilitator's** `/verify` endpoint. +9. **Facilitator** deserializes the BOC, verifies the Ed25519 signature, payment intent (amount, destination, asset), and replay protection (seqno, validUntil, BoC hash). +10. **Facilitator** returns a `VerifyResponse`. Verification is **REQUIRED** — it prevents the resource server from doing unnecessary work for invalid payloads. +11. **Resource Server**, upon successful verification, fulfills the client's request. +12. **Resource Server** calls the **Facilitator's** `/settle` endpoint. The facilitator MUST perform full verification independently and MUST NOT assume prior `/verify` results. +13. **Facilitator** settles the payment using dual-mode logic: + - **`internal_signed` (opcode `0x73696e74`):** Gasless mode. Facilitator wraps the signed body in an internal message from its own wallet, attaching TON for gas (estimated via emulation). The facilitator's W5 wallet sends this internal message to the user's W5 wallet, which verifies the signature and executes the Jetton transfer. + - **`external_signed` (opcode `0x7369676e`):** Direct mode. Facilitator broadcasts the user's external message BOC as-is. The user pays their own gas in this mode. +14. **Resource Server** returns the final response to the **Client** with `X-PAYMENT-RESPONSE` header containing the settlement result. ## `PaymentRequirements` for `exact` @@ -52,10 +55,7 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: "payTo": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", "maxTimeoutSeconds": 300, "extra": { - "relayAddress": "0:7ae5056c3fd9406f9bbbe7c7089cd4c40801d9075486cbedb7ce12df119eacf1", - "maxRelayCommission": "50000", - "assetDecimals": 6, - "assetSymbol": "USDT" + "facilitatorUrl": "https://facilitator.example.com" } } ``` @@ -65,22 +65,22 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: - `asset`: [TEP-74] Jetton master contract address (raw format `workchain:hex`). - `payTo`: Recipient TON address (raw format). - `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). -- `extra.relayAddress`: (Optional) Gasless relay address that receives the relay commission. When present, the W5 batch includes a separate `jetton_transfer` to this address as commission for gas sponsorship. When absent, the client handles gas fees directly. -- `extra.maxRelayCommission`: (Optional) Maximum relay commission in atomic token units. When present, facilitator MUST reject W5 batches where the commission transfer exceeds this amount. When absent, the facilitator applies its own commission policy. -- `extra.assetDecimals`: Token decimal places for display purposes. -- `extra.assetSymbol`: Human-readable token symbol. +- `extra.facilitatorUrl`: URL of the facilitator server. The client calls `{facilitatorUrl}/prepare` to get signing data. The resource server calls `{facilitatorUrl}/verify` and `{facilitatorUrl}/settle`. ## PaymentPayload `payload` Field -The `payload` field contains the signed external message BOC and wallet metadata: +The `payload` field contains the signed message and metadata needed for verification and settlement: ```json { - "signedBoc": "BASE64_EXTERNAL_MESSAGE_BOC", - "walletPublicKey": "HEX_ED25519_PUBLIC_KEY", - "walletAddress": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f", - "seqno": 0, - "validUntil": 1772689900 + "from": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f", + "to": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", + "tokenMaster": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", + "amount": "10000", + "validUntil": 1772689900, + "nonce": "a1b2c3d4e5f6", + "settlementBoc": "te6cckEBAgEAkwABnYgBFpKiX...", + "walletPublicKey": "14f77792ea084b4defa9bf5e99335682dd556b8ddf1943dca052ca56276136a8" } ``` @@ -102,29 +102,32 @@ Full `PaymentPayload` object: "payTo": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", "maxTimeoutSeconds": 300, "extra": { - "relayAddress": "0:7ae5056c3fd9406f9bbbe7c7089cd4c40801d9075486cbedb7ce12df119eacf1", - "maxRelayCommission": "50000", - "assetDecimals": 6, - "assetSymbol": "USDT" + "facilitatorUrl": "https://facilitator.example.com" } }, "payload": { - "signedBoc": "te6cckEBAgEAkwABnYgBFpKiX...", - "walletPublicKey": "14f77792ea084b4defa9bf5e99335682dd556b8ddf1943dca052ca56276136a8", - "walletAddress": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f", - "seqno": 3, - "validUntil": 1772689900 + "from": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f", + "to": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", + "tokenMaster": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", + "amount": "10000", + "validUntil": 1772689900, + "nonce": "a1b2c3d4e5f6", + "settlementBoc": "te6cckEBAgEAkwABnYgBFpKiX...", + "walletPublicKey": "14f77792ea084b4defa9bf5e99335682dd556b8ddf1943dca052ca56276136a8" } } ``` **Field Definitions:** -- `signedBoc`: Base64-encoded external message containing a W5 signed transfer. -- `walletPublicKey`: Ed25519 public key in hex, used by gasless relay for submission. -- `walletAddress`: Sender W5 wallet address in raw format. -- `seqno`: Current wallet sequence number (replay protection). +- `from`: Sender W5 wallet address in raw format. +- `to`: Recipient wallet address in raw format. Must match `requirements.payTo`. +- `tokenMaster`: Jetton master contract address in raw format. Must match `requirements.asset`. +- `amount`: Payment amount in atomic token units. Must match `requirements.amount`. - `validUntil`: Unix timestamp after which the signed message expires. +- `nonce`: Random string for replay protection. +- `settlementBoc`: Base64-encoded signed W5 external message BOC containing the Jetton transfer. Contains the `internal_signed` or `external_signed` body with Ed25519 signature. +- `walletPublicKey`: Ed25519 public key in hex, used for signature verification. ## `SettlementResponse` @@ -138,7 +141,7 @@ Full `PaymentPayload` object: ``` - `transaction`: Transaction hash (64-character hex string). -- `payer`: The address of the client who signed the payment (not the facilitator/relay). +- `payer`: The address of the client who signed the payment (not the facilitator). ## Facilitator Verification Rules (MUST) @@ -148,80 +151,168 @@ A facilitator verifying `exact` on TON MUST enforce all checks below before sett - `x402Version` MUST be `2`. - `payload.accepted.scheme` and `requirements.scheme` MUST both equal `"exact"`. -- `payload.accepted.network` MUST equal `requirements.network`. +- `payload.accepted.network` MUST equal `requirements.network` and MUST be a supported TVM network. - `payload.accepted.asset` MUST equal `requirements.asset`. - `payload.accepted.payTo` MUST equal `requirements.payTo`. - `payload.accepted.amount` MUST equal `requirements.amount` exactly. -### 2. Signed message validity +### 2. Signature validity -- `payload.signedBoc` MUST decode as a valid TON external message. -- The message body MUST contain a valid W5 (v5r1+) signed transfer with `authType: internal`. -- The Ed25519 signature MUST verify against `payload.walletPublicKey`. +- `payload.settlementBoc` MUST decode as a valid TON external message. +- The message body MUST contain a valid W5 (v5r1+) signed transfer with opcode `0x73696e74` (`internal_signed`) or `0x7369676e` (`external_signed`). +- The Ed25519 signature MUST verify against `payload.walletPublicKey`. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). - `payload.validUntil` MUST be in the future but within `maxTimeoutSeconds` of the current time. -### 3. Payment intent integrity +### 3. Payment intent -- The W5 message MUST contain outgoing internal messages. -- At least one internal message MUST be a `jetton_transfer` (opcode `0xf8a7ea5`). +- The W5 message MUST contain exactly **1** `jetton_transfer` (opcode `0xf8a7ea5`) internal message. No additional actions are permitted. - The `jetton_transfer` destination (after Jetton wallet resolution) MUST match `requirements.payTo`. -- The transfer amount MUST equal `requirements.amount` exactly. -- The Jetton master contract MUST match `requirements.asset`. - -### 4. Replay and anti-abuse checks - -- `payload.seqno` MUST match the wallet's current on-chain seqno. -- Duplicate `signedBoc` submissions MUST be rejected. -- The W5 message MUST contain exactly two actions: the payment `jetton_transfer` and (optionally) a relay commission `jetton_transfer`. Any additional actions MUST cause rejection. -- If `extra.maxRelayCommission` is present, the relay commission transfer amount MUST NOT exceed `extra.maxRelayCommission`. If absent, the facilitator SHOULD apply its own commission policy. -- If `payload.seqno == 0` and `stateInit` is present, the facilitator MUST verify that the contract code in `stateInit` matches a known W5 wallet contract. Facilitators SHOULD maintain an allowlist of accepted wallet code hashes and reject unknown contracts. - -### 5. Relay sponsorship safety +- The transfer amount MUST be greater than or equal to `requirements.amount`. +- The Jetton master contract (`payload.tokenMaster`) MUST match `requirements.asset`. -> [!IMPORTANT] -> These checks prevent the relay/facilitator from being tricked into transferring their own funds or sponsoring unintended actions. +### 4. Replay protection -- The facilitator/relay account MUST NOT appear as the source of the Jetton transfer. -- The facilitator MUST NOT be the payer (`walletAddress`) for the delegated transfer. -- The facilitator address MUST NOT appear as destination in any `jetton_transfer` within the W5 message (except for the relay commission transfer to `extra.relayAddress`). -- Gas costs MUST be bounded by facilitator policy to prevent sponsorship drain. +- `payload.validUntil` MUST NOT be expired and MUST NOT be more than 600 seconds in the future. +- The wallet's on-chain seqno SHOULD be checked: the seqno in the BoC MUST NOT be less than the current on-chain seqno. This check is advisory — the wallet contract is the ultimate authority on seqno validation. +- Duplicate `settlementBoc` submissions MUST be rejected via BoC hash dedup (see [Duplicate Settlement Mitigation](#duplicate-settlement-mitigation-recommended)). -### 6. Pre-settlement simulation +### 5. Pre-settlement simulation (optional) -- Facilitator SHOULD simulate message execution before broadcast. -- Settlement MUST fail if: insufficient Jetton balance, expired message, or invalid seqno. -- Simulation MUST confirm the expected balance changes: recipient receives exactly `requirements.amount`, payer balance decreases accordingly. +- Facilitator SHOULD simulate message execution via emulation before broadcast. +- Settlement SHOULD fail if simulation indicates: insufficient Jetton balance, expired message, or invalid seqno. ## Settlement Logic 1. Re-run all verification checks (do not trust prior `/verify` result). -2. Submit `signedBoc` via gasless relay or direct broadcast: - - **Sponsored (gasless):** `POST /v2/gasless/send` with `{ wallet_public_key, boc }` to a relay service. The relay wraps the signed message in an internal message carrying TON for gas. - - **Non-sponsored (direct):** Broadcast the external message directly to TON validators. The client must have sufficient TON balance for gas fees in this mode. +2. Detect the auth opcode from the signed message body: + - **`internal_signed` (0x73696e74) — Gasless mode:** + 1. Extract the signed body from the external message. + 2. Fetch the facilitator's own wallet seqno. + 3. Estimate gas via emulation: build a trial relay message, emulate the trace, sum all fees across the trace, and add a 50% buffer for gas price fluctuations. + 4. Build the relay message: wrap the user's signed body in an internal message from the facilitator's wallet to the user's wallet, attaching the estimated TON for gas. + 5. Sign and broadcast the facilitator's external message. + - **`external_signed` (0x7369676e) — Direct mode:** + 1. Broadcast the user's external message BOC as-is to the TON network. + 2. The user pays their own gas fees in this mode. 3. Wait for transaction confirmation (typically < 5 seconds on TON). 4. Return x402 `SettlementResponse` with `success`, `transaction`, `network`, and `payer`. +## Duplicate Settlement Mitigation (RECOMMENDED) + +### Vulnerability + +A race condition exists in the settlement flow: if the same payment BoC is submitted to the facilitator's `/settle` endpoint multiple times before the first submission is confirmed on-chain, each call may attempt broadcast. Although TON's seqno-based replay protection ensures the transfer only executes once on-chain, a malicious client can exploit the timing window to obtain access to multiple resources while only paying once. + +### Recommended Mitigation + +Facilitators SHOULD maintain a short-term, in-memory cache of BoC hashes that have been verified and/or settled. Before proceeding with settlement, the facilitator checks whether the BoC has already been seen: + +1. After verification succeeds, compute a hash of the `settlementBoc`. +2. If the hash is already present in the cache, reject the settlement with a `"duplicate_settlement"` error. +3. If the hash is not present, insert it into the cache and proceed with signing and submission. +4. Evict entries older than 600 seconds (the maximum `validUntil` window). After this window, the signed message will have expired and cannot land on-chain regardless. + +This approach requires no external storage or long-lived state — only an in-process set with time-based eviction. It preserves the facilitator's otherwise stateless design while closing the duplicate settlement attack vector. + +## `/prepare` Endpoint + +TON requires a facilitator-side `/prepare` step that does not exist in EVM or SVM flows. This is because TON Jetton transfers require resolving the sender's Jetton wallet address via an on-chain getter, and fetching the current wallet seqno — operations that require a TON RPC connection that clients may not have. + +### Request + +``` +POST {facilitatorUrl}/prepare +Content-Type: application/json + +{ + "from": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f", + "to": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", + "tokenMaster": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", + "amount": "10000" +} +``` + +- `from`: Client's W5 wallet address (any format — facilitator normalizes). +- `to`: Recipient's wallet address. +- `tokenMaster`: Jetton master contract address. +- `amount`: Payment amount in atomic token units. + +### Response + +```json +{ + "seqno": 3, + "validUntil": 1772690200, + "walletId": 2147483409, + "messages": [ + { + "address": "0:abc123...def456", + "amount": "100000000", + "payload": "te6cckEBAQEA..." + } + ] +} +``` + +- `seqno`: Client wallet's current sequence number. +- `validUntil`: Unix timestamp for message expiry (typically current time + 300 seconds). +- `walletId`: W5 wallet ID for the target network (2147483409 for mainnet). +- `messages`: Array of internal messages the client should sign. Each contains: + - `address`: The client's Jetton wallet address (resolved by facilitator). + - `amount`: Forward TON amount for the Jetton transfer (in nanoTON). + - `payload`: Base64-encoded Jetton transfer body with destination, amount, and response_destination. + +The client uses this data to construct and sign a W5 `internal_signed` message without making any blockchain calls. + +## Reference Implementations + +### Facilitator + +Self-relay facilitator with dual-mode settlement, emulation-based gas estimation, and 5-rule verification: +- Repository: [ohld/x402-ton-facilitator](https://github.com/ohld/x402-ton-facilitator) +- Mainnet address: [`UQAqn8F5nDx8ZvQut25e33uzcBioLLreha4yYujGdrIuHzXX`](https://tonviewer.com/UQAqn8F5nDx8ZvQut25e33uzcBioLLreha4yYujGdrIuHzXX) + +### Proof of Concept + +Side-by-side comparison: EVM, SVM, and TON payment in the same x402 client: +- Repository: [ohld/x402-ton-poc](https://github.com/ohld/x402-ton-poc) + +### SDK + +TypeScript and Python client implementations: +- Pull request: [coinbase/x402#1583](https://github.com/coinbase/x402/pull/1583) + +### On-chain Proof (7 mainnet USDT payments) + +All payments executed via the self-relay facilitator on TON mainnet: + +| # | Transaction | Amount | Date | +|---|---|---|---| +| 1 | [`ba96f62d...`](https://tonviewer.com/transaction/ba96f62d4ea651a21da4282809f2541ea42481ca35018129f29b406ef3fe36c0) | 0.01 USDT | First successful x402 payment on TON | +| 2–7 | See [facilitator transaction history](https://tonviewer.com/UQAqn8F5nDx8ZvQut25e33uzcBioLLreha4yYujGdrIuHzXX) | 0.01 USDT | Subsequent test payments | + ## Appendix -### W5 Wallet and Gasless Architecture +### W5 Wallet and Self-Relay Architecture The W5 wallet contract (v5, deployed since Aug 2024) introduced `internal_signed` messages — the key primitive for gasless transfers on TON: -1. **User signs** a message containing outgoing transfers (e.g., USDT to recipient + small USDT fee to relay). -2. **Signed message is sent off-chain** via HTTPS to a gasless relay service. -3. **Relay wraps it** in an internal message (which carries TON for gas) and submits to the user's W5 wallet contract. -4. **W5 contract verifies** the Ed25519 signature and executes the transfers. -5. **Relay is compensated** via USDT from a commission transfer included in the signed batch. +1. **Client calls `/prepare`** on the facilitator to get signing data (seqno, Jetton wallet, transfer payload). +2. **Client signs** the message offline with their Ed25519 key. Zero blockchain calls on the client side. +3. **Signed message is sent** to the resource server as an x402 payment header. +4. **Facilitator wraps it** in an internal message from its own funded wallet (carrying TON for gas) and submits to the user's W5 wallet contract. +5. **W5 contract verifies** the Ed25519 signature and executes the Jetton transfer. -This is architecturally equivalent to x402's facilitator model: +The facilitator IS the relay — no third-party relay service is needed. Gas cost (~0.013 TON ≈ $0.04 per transaction) is absorbed by the facilitator. -| x402 Concept | TON Equivalent | -|---|---| -| Facilitator | Gasless relay | -| EIP-3009 `transferWithAuthorization` | W5 `internal_signed` message | -| Gas sponsorship | Relay wraps in internal message carrying TON | +This is architecturally equivalent to x402's facilitator model on other chains: -The relay is **optional**. Any entity that can submit an internal message to a W5 contract can act as a relay. Reference implementations include the [TONAPI Gasless API][TONAPI]. +| x402 Concept | TON Equivalent | EVM Equivalent | +|---|---|---| +| Facilitator sponsors gas | Facilitator sends internal message carrying TON | Facilitator calls `transferWithAuthorization` | +| Client signs offline | W5 `internal_signed` message | EIP-3009 authorization signature | +| Zero client blockchain calls | Client calls `/prepare` (HTTP only) | Client signs EIP-712 typed data | +| Settlement | Facilitator wraps + broadcasts | Facilitator submits tx | ### TON Address Formats @@ -250,9 +341,9 @@ TON uses the [TEP-74 Jetton standard][TEP-74] for fungible tokens: - [x402 v2 core specification](../../x402-specification-v2.md) - [TEP-74 Jetton Standard][TEP-74] - [W5 Wallet Contract](https://github.com/ton-blockchain/wallet-contract-v5) -- [TONAPI Gasless API][TONAPI] - [TVM CAIP-2 Namespace](https://namespaces.chainagnostic.org/tvm/caip2) +- [Facilitator (self-relay)](https://github.com/ohld/x402-ton-facilitator) - [Working Demo](https://github.com/ohld/x402-ton-poc) +- [SDK PR](https://github.com/coinbase/x402/pull/1583) [TEP-74]: https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md -[TONAPI]: https://docs.tonconsole.com/tonapi/rest-api/gasless From c3d4c49c8cb31a86d2dd54842ba8e72880fbd097 Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:58:45 +0900 Subject: [PATCH 04/19] =?UTF-8?q?chore(spec):=20simplify=20TON=20spec=20?= =?UTF-8?q?=E2=80=94=20remove=20external=5Fsigned,=20condense=20references?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove external_signed (0x7369676e) mode — all x402 payments use gasless internal_signed. External mode deferred to future extension. - Remove on-chain proof table (7 tx links) — promotional, not normative. Proofs already linked in PR comments. - Condense Reference Implementations to 3 lines (was 20+). - Settlement Logic now single-path (no dual-mode branching). - 349 → 320 lines. Closer to SVM/Stellar spec size. Co-Authored-By: Claude Opus 4.6 (1M context) --- specs/schemes/exact/scheme_exact_ton.md | 59 +++++++------------------ 1 file changed, 15 insertions(+), 44 deletions(-) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index 6ab38579f0..024f28d22f 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -37,9 +37,7 @@ There is no relay commission. The facilitator absorbs gas costs as the cost of o 10. **Facilitator** returns a `VerifyResponse`. Verification is **REQUIRED** — it prevents the resource server from doing unnecessary work for invalid payloads. 11. **Resource Server**, upon successful verification, fulfills the client's request. 12. **Resource Server** calls the **Facilitator's** `/settle` endpoint. The facilitator MUST perform full verification independently and MUST NOT assume prior `/verify` results. -13. **Facilitator** settles the payment using dual-mode logic: - - **`internal_signed` (opcode `0x73696e74`):** Gasless mode. Facilitator wraps the signed body in an internal message from its own wallet, attaching TON for gas (estimated via emulation). The facilitator's W5 wallet sends this internal message to the user's W5 wallet, which verifies the signature and executes the Jetton transfer. - - **`external_signed` (opcode `0x7369676e`):** Direct mode. Facilitator broadcasts the user's external message BOC as-is. The user pays their own gas in this mode. +13. **Facilitator** settles the payment: wraps the client's signed body in an internal message from its own wallet, attaching TON for gas (estimated via emulation). The facilitator's W5 wallet sends this internal message to the user's W5 wallet, which verifies the signature and executes the Jetton transfer. 14. **Resource Server** returns the final response to the **Client** with `X-PAYMENT-RESPONSE` header containing the settlement result. ## `PaymentRequirements` for `exact` @@ -126,7 +124,7 @@ Full `PaymentPayload` object: - `amount`: Payment amount in atomic token units. Must match `requirements.amount`. - `validUntil`: Unix timestamp after which the signed message expires. - `nonce`: Random string for replay protection. -- `settlementBoc`: Base64-encoded signed W5 external message BOC containing the Jetton transfer. Contains the `internal_signed` or `external_signed` body with Ed25519 signature. +- `settlementBoc`: Base64-encoded signed W5 external message BOC containing the Jetton transfer with `internal_signed` body and Ed25519 signature. - `walletPublicKey`: Ed25519 public key in hex, used for signature verification. ## `SettlementResponse` @@ -159,7 +157,7 @@ A facilitator verifying `exact` on TON MUST enforce all checks below before sett ### 2. Signature validity - `payload.settlementBoc` MUST decode as a valid TON external message. -- The message body MUST contain a valid W5 (v5r1+) signed transfer with opcode `0x73696e74` (`internal_signed`) or `0x7369676e` (`external_signed`). +- The message body MUST contain a valid W5 (v5r1+) signed transfer with opcode `0x73696e74` (`internal_signed`). - The Ed25519 signature MUST verify against `payload.walletPublicKey`. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). - `payload.validUntil` MUST be in the future but within `maxTimeoutSeconds` of the current time. @@ -184,18 +182,13 @@ A facilitator verifying `exact` on TON MUST enforce all checks below before sett ## Settlement Logic 1. Re-run all verification checks (do not trust prior `/verify` result). -2. Detect the auth opcode from the signed message body: - - **`internal_signed` (0x73696e74) — Gasless mode:** - 1. Extract the signed body from the external message. - 2. Fetch the facilitator's own wallet seqno. - 3. Estimate gas via emulation: build a trial relay message, emulate the trace, sum all fees across the trace, and add a 50% buffer for gas price fluctuations. - 4. Build the relay message: wrap the user's signed body in an internal message from the facilitator's wallet to the user's wallet, attaching the estimated TON for gas. - 5. Sign and broadcast the facilitator's external message. - - **`external_signed` (0x7369676e) — Direct mode:** - 1. Broadcast the user's external message BOC as-is to the TON network. - 2. The user pays their own gas fees in this mode. -3. Wait for transaction confirmation (typically < 5 seconds on TON). -4. Return x402 `SettlementResponse` with `success`, `transaction`, `network`, and `payer`. +2. Extract the signed body from the external message. +3. Fetch the facilitator's own wallet seqno. +4. Estimate gas via emulation: build a trial relay message, emulate the trace, sum all fees across the trace, and add a 50% buffer for gas price fluctuations. +5. Build the relay message: wrap the user's signed body in an internal message from the facilitator's wallet to the user's wallet, attaching the estimated TON for gas. +6. Sign and broadcast the facilitator's external message. +7. Wait for transaction confirmation (typically < 5 seconds on TON). +8. Return x402 `SettlementResponse` with `success`, `transaction`, `network`, and `payer`. ## Duplicate Settlement Mitigation (RECOMMENDED) @@ -266,30 +259,9 @@ The client uses this data to construct and sign a W5 `internal_signed` message w ## Reference Implementations -### Facilitator - -Self-relay facilitator with dual-mode settlement, emulation-based gas estimation, and 5-rule verification: -- Repository: [ohld/x402-ton-facilitator](https://github.com/ohld/x402-ton-facilitator) -- Mainnet address: [`UQAqn8F5nDx8ZvQut25e33uzcBioLLreha4yYujGdrIuHzXX`](https://tonviewer.com/UQAqn8F5nDx8ZvQut25e33uzcBioLLreha4yYujGdrIuHzXX) - -### Proof of Concept - -Side-by-side comparison: EVM, SVM, and TON payment in the same x402 client: -- Repository: [ohld/x402-ton-poc](https://github.com/ohld/x402-ton-poc) - -### SDK - -TypeScript and Python client implementations: -- Pull request: [coinbase/x402#1583](https://github.com/coinbase/x402/pull/1583) - -### On-chain Proof (7 mainnet USDT payments) - -All payments executed via the self-relay facilitator on TON mainnet: - -| # | Transaction | Amount | Date | -|---|---|---|---| -| 1 | [`ba96f62d...`](https://tonviewer.com/transaction/ba96f62d4ea651a21da4282809f2541ea42481ca35018129f29b406ef3fe36c0) | 0.01 USDT | First successful x402 payment on TON | -| 2–7 | See [facilitator transaction history](https://tonviewer.com/UQAqn8F5nDx8ZvQut25e33uzcBioLLreha4yYujGdrIuHzXX) | 0.01 USDT | Subsequent test payments | +- **Facilitator**: [ohld/x402-ton-facilitator](https://github.com/ohld/x402-ton-facilitator) +- **POC**: [ohld/x402-ton-poc](https://github.com/ohld/x402-ton-poc) +- **SDK**: [coinbase/x402#1583](https://github.com/coinbase/x402/pull/1583) ## Appendix @@ -342,8 +314,7 @@ TON uses the [TEP-74 Jetton standard][TEP-74] for fungible tokens: - [TEP-74 Jetton Standard][TEP-74] - [W5 Wallet Contract](https://github.com/ton-blockchain/wallet-contract-v5) - [TVM CAIP-2 Namespace](https://namespaces.chainagnostic.org/tvm/caip2) -- [Facilitator (self-relay)](https://github.com/ohld/x402-ton-facilitator) -- [Working Demo](https://github.com/ohld/x402-ton-poc) -- [SDK PR](https://github.com/coinbase/x402/pull/1583) +- [Facilitator](https://github.com/ohld/x402-ton-facilitator) +- [POC](https://github.com/ohld/x402-ton-poc) [TEP-74]: https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md From 2796f26a38b4745b70eb3c1e0e6aa07fdbb55ed4 Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:09:12 +0900 Subject: [PATCH 05/19] =?UTF-8?q?fix(spec):=20remove=2050%=20gas=20buffer?= =?UTF-8?q?=20=E2=80=94=20emulation=20is=20already=20worst-case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/schemes/exact/scheme_exact_ton.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index 024f28d22f..d3a903ca36 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -184,7 +184,7 @@ A facilitator verifying `exact` on TON MUST enforce all checks below before sett 1. Re-run all verification checks (do not trust prior `/verify` result). 2. Extract the signed body from the external message. 3. Fetch the facilitator's own wallet seqno. -4. Estimate gas via emulation: build a trial relay message, emulate the trace, sum all fees across the trace, and add a 50% buffer for gas price fluctuations. +4. Estimate gas via emulation: build a trial relay message, emulate the trace, and sum all fees across the trace. 5. Build the relay message: wrap the user's signed body in an internal message from the facilitator's wallet to the user's wallet, attaching the estimated TON for gas. 6. Sign and broadcast the facilitator's external message. 7. Wait for transaction confirmation (typically < 5 seconds on TON). From d5325154e8c60c3c88f71627ab17706a7fd18e9b Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:19:49 +0900 Subject: [PATCH 06/19] =?UTF-8?q?specs(exact/ton):=20address=20all=20revie?= =?UTF-8?q?w=20comments=20=E2=80=94=20remove=20/prepare,=20add=20RPC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Remove /prepare endpoint: client resolves seqno + jetton wallet via TON RPC directly (same pattern as SVM/Stellar/Aptos) - Remove nonce field: dedup uses BoC hash, not nonce - Transfer amount: strict equality (== not >=) - Add jetton wallet verification: recipient MUST match get_wallet_address(payTo) on the asset master - Seqno check: MUST NOT be less than on-chain seqno (was SHOULD) - Add client balance check requirement - Move simulation to /verify (was optional pre-settlement) - Remove HTTP header references (transport-agnostic) - Replace hardcoded 600s with maxTimeoutSeconds - Add Client RPC Requirements appendix section --- specs/schemes/exact/scheme_exact_ton.md | 110 ++++++++---------------- 1 file changed, 36 insertions(+), 74 deletions(-) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index d3a903ca36..2cf5e6c92f 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -19,7 +19,7 @@ This spec uses [CAIP-2](https://namespaces.chainagnostic.org/tvm/caip2) identifi The `exact` scheme on TON transfers a specific amount of a [TEP-74] Jetton from the client to the resource server using a W5 wallet signed message. -The facilitator IS the relay. It sponsors gas (~0.013 TON per transaction) by wrapping the client-signed message in an internal TON message from its own funded wallet. The client makes zero blockchain calls — it calls the facilitator's `/prepare` endpoint to get signing data, signs locally, and sends the result. The facilitator cannot modify the destination or amount; the client controls payment intent through Ed25519 signature. +The facilitator IS the relay. It sponsors gas (~0.013 TON per transaction) by wrapping the client-signed message in an internal TON message from its own funded wallet. The client resolves signing data (seqno, Jetton wallet address) via a TON RPC endpoint, signs locally, and sends the result. The facilitator cannot modify the destination or amount; the client controls payment intent through Ed25519 signature. There is no relay commission. The facilitator absorbs gas costs as the cost of operating the payment network, analogous to how EVM facilitators pay gas for `transferWithAuthorization`. @@ -27,18 +27,18 @@ There is no relay commission. The facilitator absorbs gas costs as the cost of o 1. **Client** requests a protected resource from the **Resource Server**. 2. **Resource Server** responds with HTTP 402 and `PaymentRequired` data. The `accepts` array includes a TON payment option with `facilitatorUrl`. -3. **Client** calls the **Facilitator's** `/prepare` endpoint with `{ from, to, tokenMaster, amount }`. This resolves the client's Jetton wallet, fetches the current seqno, and returns signing data (seqno, validUntil, walletId, messages array). -4. **Client** constructs a W5 `internal_signed` message containing the Jetton transfer from the `/prepare` response. +3. **Client** queries a TON RPC endpoint to resolve its Jetton wallet address (`get_wallet_address` on the Jetton master contract) and fetches its current wallet seqno. +4. **Client** constructs a `jetton_transfer` body ([TEP-74]) and wraps it in a W5 `internal_signed` message. 5. **Client** signs the message with their Ed25519 private key. 6. **Client** wraps the signed body in an external message BOC (with `stateInit` if `seqno == 0`) and base64-encodes it. -7. **Client** sends a second request to the **Resource Server** with the `PaymentPayload` in the `X-PAYMENT` header. +7. **Client** sends a second request to the **Resource Server** with the `PaymentPayload`. 8. **Resource Server** forwards the payload and requirements to the **Facilitator's** `/verify` endpoint. -9. **Facilitator** deserializes the BOC, verifies the Ed25519 signature, payment intent (amount, destination, asset), and replay protection (seqno, validUntil, BoC hash). +9. **Facilitator** deserializes the BOC, verifies the Ed25519 signature, simulates the transaction, and validates payment intent (amount, destination, asset) and replay protection (seqno, validUntil, BoC hash). 10. **Facilitator** returns a `VerifyResponse`. Verification is **REQUIRED** — it prevents the resource server from doing unnecessary work for invalid payloads. 11. **Resource Server**, upon successful verification, fulfills the client's request. 12. **Resource Server** calls the **Facilitator's** `/settle` endpoint. The facilitator MUST perform full verification independently and MUST NOT assume prior `/verify` results. 13. **Facilitator** settles the payment: wraps the client's signed body in an internal message from its own wallet, attaching TON for gas (estimated via emulation). The facilitator's W5 wallet sends this internal message to the user's W5 wallet, which verifies the signature and executes the Jetton transfer. -14. **Resource Server** returns the final response to the **Client** with `X-PAYMENT-RESPONSE` header containing the settlement result. +14. **Resource Server** returns the final response to the **Client**. ## `PaymentRequirements` for `exact` @@ -63,7 +63,7 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: - `asset`: [TEP-74] Jetton master contract address (raw format `workchain:hex`). - `payTo`: Recipient TON address (raw format). - `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). -- `extra.facilitatorUrl`: URL of the facilitator server. The client calls `{facilitatorUrl}/prepare` to get signing data. The resource server calls `{facilitatorUrl}/verify` and `{facilitatorUrl}/settle`. +- `extra.facilitatorUrl`: URL of the facilitator server. The resource server calls `{facilitatorUrl}/verify` and `{facilitatorUrl}/settle`. ## PaymentPayload `payload` Field @@ -76,7 +76,6 @@ The `payload` field contains the signed message and metadata needed for verifica "tokenMaster": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", "amount": "10000", "validUntil": 1772689900, - "nonce": "a1b2c3d4e5f6", "settlementBoc": "te6cckEBAgEAkwABnYgBFpKiX...", "walletPublicKey": "14f77792ea084b4defa9bf5e99335682dd556b8ddf1943dca052ca56276136a8" } @@ -109,7 +108,6 @@ Full `PaymentPayload` object: "tokenMaster": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", "amount": "10000", "validUntil": 1772689900, - "nonce": "a1b2c3d4e5f6", "settlementBoc": "te6cckEBAgEAkwABnYgBFpKiX...", "walletPublicKey": "14f77792ea084b4defa9bf5e99335682dd556b8ddf1943dca052ca56276136a8" } @@ -123,7 +121,6 @@ Full `PaymentPayload` object: - `tokenMaster`: Jetton master contract address in raw format. Must match `requirements.asset`. - `amount`: Payment amount in atomic token units. Must match `requirements.amount`. - `validUntil`: Unix timestamp after which the signed message expires. -- `nonce`: Random string for replay protection. - `settlementBoc`: Base64-encoded signed W5 external message BOC containing the Jetton transfer with `internal_signed` body and Ed25519 signature. - `walletPublicKey`: Ed25519 public key in hex, used for signature verification. @@ -143,7 +140,7 @@ Full `PaymentPayload` object: ## Facilitator Verification Rules (MUST) -A facilitator verifying `exact` on TON MUST enforce all checks below before settlement. +A facilitator verifying `exact` on TON MUST enforce all checks below. Verification happens at `/verify` time to protect the resource server from unnecessary work. ### 1. Protocol and requirement consistency @@ -164,20 +161,24 @@ A facilitator verifying `exact` on TON MUST enforce all checks below before sett ### 3. Payment intent - The W5 message MUST contain exactly **1** `jetton_transfer` (opcode `0xf8a7ea5`) internal message. No additional actions are permitted. -- The `jetton_transfer` destination (after Jetton wallet resolution) MUST match `requirements.payTo`. -- The transfer amount MUST be greater than or equal to `requirements.amount`. +- The recipient Jetton wallet contract MUST match `get_wallet_address(requirements.payTo)` called on the Jetton master contract (`requirements.asset`). +- The transfer amount MUST be equal to `requirements.amount`. - The Jetton master contract (`payload.tokenMaster`) MUST match `requirements.asset`. ### 4. Replay protection -- `payload.validUntil` MUST NOT be expired and MUST NOT be more than 600 seconds in the future. -- The wallet's on-chain seqno SHOULD be checked: the seqno in the BoC MUST NOT be less than the current on-chain seqno. This check is advisory — the wallet contract is the ultimate authority on seqno validation. +- `payload.validUntil` MUST NOT be expired and MUST NOT be more than `maxTimeoutSeconds` in the future. +- The wallet's on-chain seqno MUST be checked: the seqno in the BoC MUST NOT be less than the current on-chain seqno. +- The client MUST have sufficient balance of the payment asset. - Duplicate `settlementBoc` submissions MUST be rejected via BoC hash dedup (see [Duplicate Settlement Mitigation](#duplicate-settlement-mitigation-recommended)). -### 5. Pre-settlement simulation (optional) +> **Note:** Seqno and balance checks MAY be satisfied implicitly via transaction simulation (section 5). The spec declares them as explicit requirements so that implementations that do not simulate still enforce these checks. -- Facilitator SHOULD simulate message execution via emulation before broadcast. -- Settlement SHOULD fail if simulation indicates: insufficient Jetton balance, expired message, or invalid seqno. +### 5. Transaction simulation (recommended) + +- Facilitator SHOULD simulate message execution via emulation during `/verify`. +- Verification SHOULD fail if simulation indicates: insufficient Jetton balance, expired message, or invalid seqno. +- When simulation is performed, it implicitly covers seqno and balance checks from section 4. ## Settlement Logic @@ -203,60 +204,10 @@ Facilitators SHOULD maintain a short-term, in-memory cache of BoC hashes that ha 1. After verification succeeds, compute a hash of the `settlementBoc`. 2. If the hash is already present in the cache, reject the settlement with a `"duplicate_settlement"` error. 3. If the hash is not present, insert it into the cache and proceed with signing and submission. -4. Evict entries older than 600 seconds (the maximum `validUntil` window). After this window, the signed message will have expired and cannot land on-chain regardless. +4. Evict entries older than `maxTimeoutSeconds` from the corresponding `PaymentRequirements`. After this window, the signed message will have expired and cannot land on-chain regardless. This approach requires no external storage or long-lived state — only an in-process set with time-based eviction. It preserves the facilitator's otherwise stateless design while closing the duplicate settlement attack vector. -## `/prepare` Endpoint - -TON requires a facilitator-side `/prepare` step that does not exist in EVM or SVM flows. This is because TON Jetton transfers require resolving the sender's Jetton wallet address via an on-chain getter, and fetching the current wallet seqno — operations that require a TON RPC connection that clients may not have. - -### Request - -``` -POST {facilitatorUrl}/prepare -Content-Type: application/json - -{ - "from": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f", - "to": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", - "tokenMaster": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", - "amount": "10000" -} -``` - -- `from`: Client's W5 wallet address (any format — facilitator normalizes). -- `to`: Recipient's wallet address. -- `tokenMaster`: Jetton master contract address. -- `amount`: Payment amount in atomic token units. - -### Response - -```json -{ - "seqno": 3, - "validUntil": 1772690200, - "walletId": 2147483409, - "messages": [ - { - "address": "0:abc123...def456", - "amount": "100000000", - "payload": "te6cckEBAQEA..." - } - ] -} -``` - -- `seqno`: Client wallet's current sequence number. -- `validUntil`: Unix timestamp for message expiry (typically current time + 300 seconds). -- `walletId`: W5 wallet ID for the target network (2147483409 for mainnet). -- `messages`: Array of internal messages the client should sign. Each contains: - - `address`: The client's Jetton wallet address (resolved by facilitator). - - `amount`: Forward TON amount for the Jetton transfer (in nanoTON). - - `payload`: Base64-encoded Jetton transfer body with destination, amount, and response_destination. - -The client uses this data to construct and sign a W5 `internal_signed` message without making any blockchain calls. - ## Reference Implementations - **Facilitator**: [ohld/x402-ton-facilitator](https://github.com/ohld/x402-ton-facilitator) @@ -269,13 +220,13 @@ The client uses this data to construct and sign a W5 `internal_signed` message w The W5 wallet contract (v5, deployed since Aug 2024) introduced `internal_signed` messages — the key primitive for gasless transfers on TON: -1. **Client calls `/prepare`** on the facilitator to get signing data (seqno, Jetton wallet, transfer payload). -2. **Client signs** the message offline with their Ed25519 key. Zero blockchain calls on the client side. -3. **Signed message is sent** to the resource server as an x402 payment header. +1. **Client resolves signing data** via a TON RPC endpoint: wallet seqno and Jetton wallet address. +2. **Client constructs and signs** the message offline with their Ed25519 key. Standard RPC calls only (same as SVM/Stellar/Aptos). +3. **Signed message is sent** to the resource server as an x402 payment payload. 4. **Facilitator wraps it** in an internal message from its own funded wallet (carrying TON for gas) and submits to the user's W5 wallet contract. 5. **W5 contract verifies** the Ed25519 signature and executes the Jetton transfer. -The facilitator IS the relay — no third-party relay service is needed. Gas cost (~0.013 TON ≈ $0.04 per transaction) is absorbed by the facilitator. +The facilitator IS the relay — no third-party relay service is needed. Gas cost (~0.013 TON per transaction) is absorbed by the facilitator. This is architecturally equivalent to x402's facilitator model on other chains: @@ -283,9 +234,20 @@ This is architecturally equivalent to x402's facilitator model on other chains: |---|---|---| | Facilitator sponsors gas | Facilitator sends internal message carrying TON | Facilitator calls `transferWithAuthorization` | | Client signs offline | W5 `internal_signed` message | EIP-3009 authorization signature | -| Zero client blockchain calls | Client calls `/prepare` (HTTP only) | Client signs EIP-712 typed data | +| Client RPC calls | seqno + `get_wallet_address` (2 calls) | nonce lookup (1 call, permit2 extension only) | | Settlement | Facilitator wraps + broadcasts | Facilitator submits tx | +### Client RPC Requirements + +The client requires access to a TON RPC endpoint to prepare the payment. Two read-only calls are needed: + +1. **Wallet seqno**: call the `seqno` getter on the client's W5 wallet contract. +2. **Jetton wallet address**: call `get_wallet_address` on the Jetton master contract with the sender's address to resolve the sender's Jetton wallet. + +This is the same pattern as other x402 networks: SVM clients fetch the recent blockhash, Stellar clients simulate the transaction, and Aptos clients query sequence numbers — all via standard RPC calls. + +Implementations SHOULD allow configuring a custom RPC endpoint and optional API key for higher rate limits. + ### TON Address Formats - **Raw**: `workchain:hex` (e.g., `0:b113a994...`) — used in this protocol. From 3e1b00ff095f2099731a3adaf20dfd3ef0f759a2 Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:46:44 +0900 Subject: [PATCH 07/19] specs(exact/ton): add facilitator safety, explicit jetton wallet check - Add section 3 (Facilitator Safety): facilitator address MUST NOT appear as sender or Jetton transfer source. Matches SVM/Stellar. - Make get_wallet_address check explicit per @skywardboundd: the destination in the deserialized BoC must match the resolved wallet. - Align verification intro phrasing with SVM/Stellar style. --- specs/schemes/exact/scheme_exact_ton.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index 2cf5e6c92f..da75ba8273 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -140,7 +140,7 @@ Full `PaymentPayload` object: ## Facilitator Verification Rules (MUST) -A facilitator verifying `exact` on TON MUST enforce all checks below. Verification happens at `/verify` time to protect the resource server from unnecessary work. +A facilitator verifying `exact` on TON MUST enforce all of the following checks before sponsoring and relaying the transaction: ### 1. Protocol and requirement consistency @@ -158,27 +158,31 @@ A facilitator verifying `exact` on TON MUST enforce all checks below. Verificati - The Ed25519 signature MUST verify against `payload.walletPublicKey`. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). - `payload.validUntil` MUST be in the future but within `maxTimeoutSeconds` of the current time. -### 3. Payment intent +### 3. Facilitator safety + +- The facilitator's own address MUST NOT appear as the sender (`payload.from`) or as the source of any Jetton transfer. This prevents a malicious payload from tricking the facilitator into spending its own funds. + +### 4. Payment intent - The W5 message MUST contain exactly **1** `jetton_transfer` (opcode `0xf8a7ea5`) internal message. No additional actions are permitted. -- The recipient Jetton wallet contract MUST match `get_wallet_address(requirements.payTo)` called on the Jetton master contract (`requirements.asset`). +- The destination address of the `jetton_transfer` internal message in the BoC MUST match the Jetton wallet address returned by `get_wallet_address(requirements.payTo)` on the Jetton master contract (`requirements.asset`). This ensures the transfer targets the legitimate Jetton wallet, not a substitute contract. - The transfer amount MUST be equal to `requirements.amount`. - The Jetton master contract (`payload.tokenMaster`) MUST match `requirements.asset`. -### 4. Replay protection +### 5. Replay protection - `payload.validUntil` MUST NOT be expired and MUST NOT be more than `maxTimeoutSeconds` in the future. - The wallet's on-chain seqno MUST be checked: the seqno in the BoC MUST NOT be less than the current on-chain seqno. - The client MUST have sufficient balance of the payment asset. - Duplicate `settlementBoc` submissions MUST be rejected via BoC hash dedup (see [Duplicate Settlement Mitigation](#duplicate-settlement-mitigation-recommended)). -> **Note:** Seqno and balance checks MAY be satisfied implicitly via transaction simulation (section 5). The spec declares them as explicit requirements so that implementations that do not simulate still enforce these checks. +> **Note:** Seqno and balance checks MAY be satisfied implicitly via transaction simulation (section 6). The spec declares them as explicit requirements so that implementations that do not simulate still enforce these checks. -### 5. Transaction simulation (recommended) +### 6. Transaction simulation (recommended) - Facilitator SHOULD simulate message execution via emulation during `/verify`. - Verification SHOULD fail if simulation indicates: insufficient Jetton balance, expired message, or invalid seqno. -- When simulation is performed, it implicitly covers seqno and balance checks from section 4. +- When simulation is performed, it implicitly covers seqno and balance checks from section 5. ## Settlement Logic From 5b88a139ae75b95f9e04068a7c04aeb6c0a78eaf Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:05:33 +0900 Subject: [PATCH 08/19] specs(exact/ton): add areFeesSponsored flag per Stellar pattern Follows Stellar's approach: extra.areFeesSponsored boolean, currently always true. Non-sponsored flow (client pays gas) to be added later. --- specs/schemes/exact/scheme_exact_ton.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index da75ba8273..83bb2c4ce0 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -53,7 +53,8 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: "payTo": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", "maxTimeoutSeconds": 300, "extra": { - "facilitatorUrl": "https://facilitator.example.com" + "facilitatorUrl": "https://facilitator.example.com", + "areFeesSponsored": true } } ``` @@ -64,6 +65,7 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: - `payTo`: Recipient TON address (raw format). - `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). - `extra.facilitatorUrl`: URL of the facilitator server. The resource server calls `{facilitatorUrl}/verify` and `{facilitatorUrl}/settle`. +- `extra.areFeesSponsored`: Whether the facilitator sponsors gas fees. Currently always `true`; a non-sponsored flow will be added later. ## PaymentPayload `payload` Field @@ -99,7 +101,8 @@ Full `PaymentPayload` object: "payTo": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", "maxTimeoutSeconds": 300, "extra": { - "facilitatorUrl": "https://facilitator.example.com" + "facilitatorUrl": "https://facilitator.example.com", + "areFeesSponsored": true } }, "payload": { From 96784100d17585d9c75896c1a5ee766010ee2fa9 Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:12:29 +0900 Subject: [PATCH 09/19] specs(exact/ton): fix validUntil duplication, add stateInit code hash check - Remove duplicate validUntil check from section 2 (already in section 5) - Add stateInit code hash verification for seqno==0 deployments (per arjun215-eng's earlier feedback) --- specs/schemes/exact/scheme_exact_ton.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index 83bb2c4ce0..5cc1e4ed17 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -159,7 +159,7 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks - `payload.settlementBoc` MUST decode as a valid TON external message. - The message body MUST contain a valid W5 (v5r1+) signed transfer with opcode `0x73696e74` (`internal_signed`). - The Ed25519 signature MUST verify against `payload.walletPublicKey`. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). -- `payload.validUntil` MUST be in the future but within `maxTimeoutSeconds` of the current time. +- If the external message includes `stateInit` (seqno == 0), the facilitator MUST verify the contract code matches a known W5 wallet contract. Implementations SHOULD maintain an allowlist of accepted wallet code hashes. ### 3. Facilitator safety From c569c2423e7c9f7e815414ffb7f737d5b29b2afe Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:18:42 +0900 Subject: [PATCH 10/19] specs(exact/ton): add payload.to and source Jetton wallet checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per @skywardboundd review: 1. payload.to MUST equal requirements.payTo (explicit metadata check) 2. Source Jetton wallet (W5 internal msg destination) MUST match get_wallet_address(from) — prevents substitute source contract 3. jetton_transfer body destination MUST equal requirements.payTo Tested against deployed facilitator with real on-chain data. Facilitator commit: ohld/x402-ton-facilitator@6f92320 --- specs/schemes/exact/scheme_exact_ton.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index 5cc1e4ed17..f45fed475a 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -167,10 +167,12 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks ### 4. Payment intent +- `payload.to` MUST equal `requirements.payTo`. - The W5 message MUST contain exactly **1** `jetton_transfer` (opcode `0xf8a7ea5`) internal message. No additional actions are permitted. -- The destination address of the `jetton_transfer` internal message in the BoC MUST match the Jetton wallet address returned by `get_wallet_address(requirements.payTo)` on the Jetton master contract (`requirements.asset`). This ensures the transfer targets the legitimate Jetton wallet, not a substitute contract. - The transfer amount MUST be equal to `requirements.amount`. - The Jetton master contract (`payload.tokenMaster`) MUST match `requirements.asset`. +- The source Jetton wallet — i.e., the destination of the W5 internal message in the BoC — MUST match the Jetton wallet address returned by `get_wallet_address(payload.from)` on the Jetton master contract (`requirements.asset`). This prevents a malicious BoC from using a substitute source contract. +- The `destination` field inside the `jetton_transfer` body MUST equal `requirements.payTo`. This ensures funds are routed to the correct recipient. ### 5. Replay protection From 8c11af28e4f0f2a3a2311367c9b516456a15cb3d Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:51:13 +0900 Subject: [PATCH 11/19] specs(exact/ton): remove facilitatorUrl, fix dedup TTL, add canonical W5R1 hash - Remove facilitatorUrl from extra (implementation detail, not protocol field) - Fix dedup TTL: use fixed 300s instead of per-request maxTimeoutSeconds - Add canonical W5R1 code hash for stateInit verification - Note simulation as alternative to code hash checks for seqno>0 Per @phdargen review on coinbase/x402#1455 --- specs/schemes/exact/scheme_exact_ton.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index f45fed475a..73852010a2 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -26,7 +26,7 @@ There is no relay commission. The facilitator absorbs gas costs as the cost of o ## Protocol Flow 1. **Client** requests a protected resource from the **Resource Server**. -2. **Resource Server** responds with HTTP 402 and `PaymentRequired` data. The `accepts` array includes a TON payment option with `facilitatorUrl`. +2. **Resource Server** responds with HTTP 402 and `PaymentRequired` data. The `accepts` array includes a TON payment option. 3. **Client** queries a TON RPC endpoint to resolve its Jetton wallet address (`get_wallet_address` on the Jetton master contract) and fetches its current wallet seqno. 4. **Client** constructs a `jetton_transfer` body ([TEP-74]) and wraps it in a W5 `internal_signed` message. 5. **Client** signs the message with their Ed25519 private key. @@ -53,7 +53,6 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: "payTo": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", "maxTimeoutSeconds": 300, "extra": { - "facilitatorUrl": "https://facilitator.example.com", "areFeesSponsored": true } } @@ -64,7 +63,6 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: - `asset`: [TEP-74] Jetton master contract address (raw format `workchain:hex`). - `payTo`: Recipient TON address (raw format). - `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). -- `extra.facilitatorUrl`: URL of the facilitator server. The resource server calls `{facilitatorUrl}/verify` and `{facilitatorUrl}/settle`. - `extra.areFeesSponsored`: Whether the facilitator sponsors gas fees. Currently always `true`; a non-sponsored flow will be added later. ## PaymentPayload `payload` Field @@ -101,7 +99,6 @@ Full `PaymentPayload` object: "payTo": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", "maxTimeoutSeconds": 300, "extra": { - "facilitatorUrl": "https://facilitator.example.com", "areFeesSponsored": true } }, @@ -159,7 +156,8 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks - `payload.settlementBoc` MUST decode as a valid TON external message. - The message body MUST contain a valid W5 (v5r1+) signed transfer with opcode `0x73696e74` (`internal_signed`). - The Ed25519 signature MUST verify against `payload.walletPublicKey`. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). -- If the external message includes `stateInit` (seqno == 0), the facilitator MUST verify the contract code matches a known W5 wallet contract. Implementations SHOULD maintain an allowlist of accepted wallet code hashes. +- If the external message includes `stateInit` (seqno == 0), the facilitator MUST verify the contract code matches a known W5 wallet contract. The canonical code hash for W5R1 is `20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f`. Implementations SHOULD maintain an allowlist of accepted wallet code hashes and update it when TON Foundation publishes new wallet versions. +- For wallets with seqno > 0, simulation (section 6) provides equivalent protection: if the contract does not execute the expected Jetton transfer, simulation will fail. Implementations that simulate MAY skip explicit code hash checks. ### 3. Facilitator safety @@ -213,7 +211,7 @@ Facilitators SHOULD maintain a short-term, in-memory cache of BoC hashes that ha 1. After verification succeeds, compute a hash of the `settlementBoc`. 2. If the hash is already present in the cache, reject the settlement with a `"duplicate_settlement"` error. 3. If the hash is not present, insert it into the cache and proceed with signing and submission. -4. Evict entries older than `maxTimeoutSeconds` from the corresponding `PaymentRequirements`. After this window, the signed message will have expired and cannot land on-chain regardless. +4. Evict entries older than 300 seconds. After this window, the signed message's `validUntil` will have passed (default `maxTimeoutSeconds` is 300s) and it cannot land on-chain regardless. This is analogous to SVM's fixed 120-second eviction window (tied to blockhash lifetime). This approach requires no external storage or long-lived state — only an in-process set with time-based eviction. It preserves the facilitator's otherwise stateless design while closing the duplicate settlement attack vector. From 1d4cda7a93017eb6c14d3df2f71d4d5169a38e77 Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:51:59 +0900 Subject: [PATCH 12/19] specs(exact/ton): internal message BoC, minimal payload, strict seqno Major rework based on TON Core team review: - settlementBoc is now an internal message (not external) - Payload reduced to {settlementBoc, asset}, all fields derived from BoC - Public key derived from stateInit or on-chain get_public_key - Seqno check changed to strict equality (TON requirement) - Internal message must be bounceable - Removed facilitatorUrl, nonce, walletPublicKey - Clarified simulation in /verify, extensibility for non-gasless flows - Updated parent scheme_exact.md TON section --- specs/schemes/exact/scheme_exact.md | 10 ++-- specs/schemes/exact/scheme_exact_ton.md | 77 +++++++++++-------------- 2 files changed, 39 insertions(+), 48 deletions(-) diff --git a/specs/schemes/exact/scheme_exact.md b/specs/schemes/exact/scheme_exact.md index 03827fc364..53e8bf5be9 100644 --- a/specs/schemes/exact/scheme_exact.md +++ b/specs/schemes/exact/scheme_exact.md @@ -32,10 +32,10 @@ While implementation details vary by network, facilitators MUST enforce security ### TON -- Self-relay safety: the facilitator address MUST NOT appear as the source of any Jetton transfer or as the sender wallet address. -- Transfer correctness: exactly 1 `jetton_transfer` — destination MUST equal `payTo` (after Jetton wallet resolution) and transfer amount MUST equal `requirements.amount` exactly. -- Signature validity: Ed25519 signature MUST verify against `walletPublicKey` for both `internal_signed` (0x73696e74) and `external_signed` (0x7369676e) opcodes. -- Replay protection: seqno MUST NOT be stale; duplicate `settlementBoc` submissions rejected via BoC hash dedup. -- Simulation verification: SHOULD simulate via emulation before broadcast to confirm expected balance changes. +- Self-relay safety: the facilitator address MUST NOT appear as the sender or source of any Jetton transfer. +- Transfer correctness: exactly 1 `jetton_transfer` with destination equal to `payTo` and amount equal to `requirements.amount` exactly. +- Signature validity: Ed25519 signature MUST verify against a public key derived from the BoC's `stateInit` (seqno == 0) or from the on-chain `get_public_key` getter (seqno > 0). Only `internal_signed` (0x73696e74) opcode is supported in the current gasless flow. +- Replay protection: seqno MUST be strictly equal to on-chain value; duplicate `settlementBoc` submissions rejected via BoC hash dedup. +- Simulation verification: SHOULD simulate via emulation during `/verify` to confirm expected balance changes. Network-specific rules are in per-network documents: `scheme_exact_svm.md` (Solana), `scheme_exact_stellar.md` (Stellar), `scheme_exact_evm.md` (EVM), `scheme_exact_sui.md` (SUI), `scheme_exact_ton.md` (TON). diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index 73852010a2..9520440502 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -13,7 +13,7 @@ This spec uses [CAIP-2](https://namespaces.chainagnostic.org/tvm/caip2) identifi - `tvm:-3` — TON testnet > [!NOTE] -> **Scope:** This spec covers [TEP-74]-compliant Jetton transfers using **W5+ wallets** (v5r1 and later) only. Earlier wallet versions (v3, v4) do not support `internal_signed` messages required for gasless transactions. +> **Scope:** This spec covers [TEP-74]-compliant Jetton transfers using **W5+ wallets** (v5r1 and later) with gasless relay (`areFeesSponsored: true`). Non-gasless flows (`external_signed`, native TON transfers) are planned for a follow-up spec extension. ## Summary @@ -30,14 +30,14 @@ There is no relay commission. The facilitator absorbs gas costs as the cost of o 3. **Client** queries a TON RPC endpoint to resolve its Jetton wallet address (`get_wallet_address` on the Jetton master contract) and fetches its current wallet seqno. 4. **Client** constructs a `jetton_transfer` body ([TEP-74]) and wraps it in a W5 `internal_signed` message. 5. **Client** signs the message with their Ed25519 private key. -6. **Client** wraps the signed body in an external message BOC (with `stateInit` if `seqno == 0`) and base64-encodes it. +6. **Client** wraps the signed body in a bounceable internal message BOC (dest = client wallet, value = 0, with `stateInit` if `seqno == 0`) and base64-encodes it. 7. **Client** sends a second request to the **Resource Server** with the `PaymentPayload`. 8. **Resource Server** forwards the payload and requirements to the **Facilitator's** `/verify` endpoint. -9. **Facilitator** deserializes the BOC, verifies the Ed25519 signature, simulates the transaction, and validates payment intent (amount, destination, asset) and replay protection (seqno, validUntil, BoC hash). -10. **Facilitator** returns a `VerifyResponse`. Verification is **REQUIRED** — it prevents the resource server from doing unnecessary work for invalid payloads. +9. **Facilitator** deserializes the internal message BoC, derives the sender address and public key, verifies the Ed25519 signature, validates payment intent (amount, destination, asset), and checks replay protection (seqno, validUntil, BoC hash). +10. **Facilitator** returns a `VerifyResponse`. Verification is **REQUIRED** — it protects the resource server from doing unnecessary work for invalid payloads. This is an x402 protocol-level requirement, not specific to TON. 11. **Resource Server**, upon successful verification, fulfills the client's request. 12. **Resource Server** calls the **Facilitator's** `/settle` endpoint. The facilitator MUST perform full verification independently and MUST NOT assume prior `/verify` results. -13. **Facilitator** settles the payment: wraps the client's signed body in an internal message from its own wallet, attaching TON for gas (estimated via emulation). The facilitator's W5 wallet sends this internal message to the user's W5 wallet, which verifies the signature and executes the Jetton transfer. +13. **Facilitator** settles the payment: extracts the signed body from the internal message BoC, wraps it in a new internal message from its own wallet attaching TON for gas (estimated via emulation), signs and broadcasts. The user's W5 wallet verifies the signature and executes the Jetton transfer. 14. **Resource Server** returns the final response to the **Client**. ## `PaymentRequirements` for `exact` @@ -63,21 +63,16 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: - `asset`: [TEP-74] Jetton master contract address (raw format `workchain:hex`). - `payTo`: Recipient TON address (raw format). - `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). -- `extra.areFeesSponsored`: Whether the facilitator sponsors gas fees. Currently always `true`; a non-sponsored flow will be added later. +- `extra.areFeesSponsored`: Whether the facilitator sponsors gas fees. Currently always `true`; a non-sponsored flow will be added in a follow-up spec. ## PaymentPayload `payload` Field -The `payload` field contains the signed message and metadata needed for verification and settlement: +The payload contains only the signed settlement BoC and the asset identifier. All other fields (sender address, amount, destination, public key) are derived from the BoC by the facilitator: ```json { - "from": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f", - "to": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", - "tokenMaster": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", - "amount": "10000", - "validUntil": 1772689900, "settlementBoc": "te6cckEBAgEAkwABnYgBFpKiX...", - "walletPublicKey": "14f77792ea084b4defa9bf5e99335682dd556b8ddf1943dca052ca56276136a8" + "asset": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe" } ``` @@ -103,26 +98,21 @@ Full `PaymentPayload` object: } }, "payload": { - "from": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f", - "to": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", - "tokenMaster": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe", - "amount": "10000", - "validUntil": 1772689900, "settlementBoc": "te6cckEBAgEAkwABnYgBFpKiX...", - "walletPublicKey": "14f77792ea084b4defa9bf5e99335682dd556b8ddf1943dca052ca56276136a8" + "asset": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe" } } ``` **Field Definitions:** -- `from`: Sender W5 wallet address in raw format. -- `to`: Recipient wallet address in raw format. Must match `requirements.payTo`. -- `tokenMaster`: Jetton master contract address in raw format. Must match `requirements.asset`. -- `amount`: Payment amount in atomic token units. Must match `requirements.amount`. -- `validUntil`: Unix timestamp after which the signed message expires. -- `settlementBoc`: Base64-encoded signed W5 external message BOC containing the Jetton transfer with `internal_signed` body and Ed25519 signature. -- `walletPublicKey`: Ed25519 public key in hex, used for signature verification. +- `settlementBoc`: Base64-encoded TON internal message BoC. The internal message carries the signed W5 body as the `body` field, the client's wallet as `dest`, and optionally a `stateInit` for first-time wallet deployment. The message MUST be bounceable. +- `asset`: Jetton master contract address in raw format. Kept as an explicit field for future multi-asset protocol extensions. + +The facilitator derives the following from the BoC: +- **Sender address**: the `dest` field of the internal message (the client's wallet). +- **Public key**: from the `stateInit` data cell (if present) or via the on-chain `get_public_key` getter. +- **Amount, destination, validUntil, seqno**: from the W5 signed body and its actions. ## `SettlementResponse` @@ -153,47 +143,48 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks ### 2. Signature validity -- `payload.settlementBoc` MUST decode as a valid TON external message. +- `payload.settlementBoc` MUST decode as a valid TON internal message. +- The `dest` field of the internal message is the client's wallet address (the sender/payer). - The message body MUST contain a valid W5 (v5r1+) signed transfer with opcode `0x73696e74` (`internal_signed`). -- The Ed25519 signature MUST verify against `payload.walletPublicKey`. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). -- If the external message includes `stateInit` (seqno == 0), the facilitator MUST verify the contract code matches a known W5 wallet contract. The canonical code hash for W5R1 is `20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f`. Implementations SHOULD maintain an allowlist of accepted wallet code hashes and update it when TON Foundation publishes new wallet versions. +- The Ed25519 public key MUST be derived from the BoC's `stateInit` data cell (when `stateInit` is present, i.e. seqno == 0) or via the on-chain `get_public_key` getter on the client's wallet contract (when seqno > 0). The public key is NOT passed as a separate payload field. +- The Ed25519 signature MUST verify against the derived public key. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). +- If the message includes `stateInit` (seqno == 0), the facilitator MUST verify the contract code matches a known W5 wallet contract. The canonical code hash for W5R1 is `20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f`. Implementations SHOULD maintain an allowlist of accepted wallet code hashes and update it when TON Foundation publishes new wallet versions. Currently the allowlist contains one entry (W5R1). Wallet versions ship very rarely on TON (years apart), so this list is near-static. - For wallets with seqno > 0, simulation (section 6) provides equivalent protection: if the contract does not execute the expected Jetton transfer, simulation will fail. Implementations that simulate MAY skip explicit code hash checks. ### 3. Facilitator safety -- The facilitator's own address MUST NOT appear as the sender (`payload.from`) or as the source of any Jetton transfer. This prevents a malicious payload from tricking the facilitator into spending its own funds. +- The facilitator's own address MUST NOT appear as the sender (derived from the BoC's `dest` field) or as the source of any Jetton transfer. This prevents a malicious payload from tricking the facilitator into spending its own funds. ### 4. Payment intent -- `payload.to` MUST equal `requirements.payTo`. - The W5 message MUST contain exactly **1** `jetton_transfer` (opcode `0xf8a7ea5`) internal message. No additional actions are permitted. - The transfer amount MUST be equal to `requirements.amount`. -- The Jetton master contract (`payload.tokenMaster`) MUST match `requirements.asset`. -- The source Jetton wallet — i.e., the destination of the W5 internal message in the BoC — MUST match the Jetton wallet address returned by `get_wallet_address(payload.from)` on the Jetton master contract (`requirements.asset`). This prevents a malicious BoC from using a substitute source contract. +- The `asset` field in the payload MUST match `requirements.asset`. +- The source Jetton wallet (the destination of the W5 internal message in the BoC) MUST match the Jetton wallet address returned by `get_wallet_address(sender)` on the Jetton master contract (`requirements.asset`). This prevents a malicious BoC from using a substitute source contract. - The `destination` field inside the `jetton_transfer` body MUST equal `requirements.payTo`. This ensures funds are routed to the correct recipient. ### 5. Replay protection -- `payload.validUntil` MUST NOT be expired and MUST NOT be more than `maxTimeoutSeconds` in the future. -- The wallet's on-chain seqno MUST be checked: the seqno in the BoC MUST NOT be less than the current on-chain seqno. +- The `validUntil` timestamp in the W5 body MUST NOT be expired and MUST NOT be more than `maxTimeoutSeconds` in the future. +- The wallet's on-chain seqno MUST be checked: the seqno in the BoC MUST be strictly equal to the current on-chain seqno. On TON, wallet contracts reject messages where the seqno does not match exactly. - The client MUST have sufficient balance of the payment asset. - Duplicate `settlementBoc` submissions MUST be rejected via BoC hash dedup (see [Duplicate Settlement Mitigation](#duplicate-settlement-mitigation-recommended)). -> **Note:** Seqno and balance checks MAY be satisfied implicitly via transaction simulation (section 6). The spec declares them as explicit requirements so that implementations that do not simulate still enforce these checks. +> **Note:** Seqno, balance, and expiry checks MAY be satisfied implicitly via transaction simulation (section 6). The spec declares them as explicit requirements so that implementations that do not simulate still enforce these checks. ### 6. Transaction simulation (recommended) -- Facilitator SHOULD simulate message execution via emulation during `/verify`. +- Facilitator SHOULD simulate message execution via emulation during `/verify`. This protects the resource server from doing unnecessary work for invalid payloads. - Verification SHOULD fail if simulation indicates: insufficient Jetton balance, expired message, or invalid seqno. -- When simulation is performed, it implicitly covers seqno and balance checks from section 5. +- When simulation is performed, it implicitly covers seqno, balance, and code hash checks from sections 2 and 5. ## Settlement Logic 1. Re-run all verification checks (do not trust prior `/verify` result). -2. Extract the signed body from the external message. +2. Extract the signed body and optional `stateInit` from the internal message BoC. 3. Fetch the facilitator's own wallet seqno. 4. Estimate gas via emulation: build a trial relay message, emulate the trace, and sum all fees across the trace. -5. Build the relay message: wrap the user's signed body in an internal message from the facilitator's wallet to the user's wallet, attaching the estimated TON for gas. +5. Build the relay message: wrap the user's signed body in a bounceable internal message from the facilitator's wallet to the user's wallet, attaching the estimated TON for gas. If the user's wallet has a `stateInit` (seqno == 0), include it in the relay message for deployment. 6. Sign and broadcast the facilitator's external message. 7. Wait for transaction confirmation (typically < 5 seconds on TON). 8. Return x402 `SettlementResponse` with `success`, `transaction`, `network`, and `payer`. @@ -211,7 +202,7 @@ Facilitators SHOULD maintain a short-term, in-memory cache of BoC hashes that ha 1. After verification succeeds, compute a hash of the `settlementBoc`. 2. If the hash is already present in the cache, reject the settlement with a `"duplicate_settlement"` error. 3. If the hash is not present, insert it into the cache and proceed with signing and submission. -4. Evict entries older than 300 seconds. After this window, the signed message's `validUntil` will have passed (default `maxTimeoutSeconds` is 300s) and it cannot land on-chain regardless. This is analogous to SVM's fixed 120-second eviction window (tied to blockhash lifetime). +4. Evict entries older than `maxTimeoutSeconds` (default 300s). After this window, the signed message's `validUntil` will have passed and it cannot land on-chain regardless. This is analogous to SVM's fixed 120-second eviction window (tied to blockhash lifetime). This approach requires no external storage or long-lived state — only an in-process set with time-based eviction. It preserves the facilitator's otherwise stateless design while closing the duplicate settlement attack vector. @@ -229,8 +220,8 @@ The W5 wallet contract (v5, deployed since Aug 2024) introduced `internal_signed 1. **Client resolves signing data** via a TON RPC endpoint: wallet seqno and Jetton wallet address. 2. **Client constructs and signs** the message offline with their Ed25519 key. Standard RPC calls only (same as SVM/Stellar/Aptos). -3. **Signed message is sent** to the resource server as an x402 payment payload. -4. **Facilitator wraps it** in an internal message from its own funded wallet (carrying TON for gas) and submits to the user's W5 wallet contract. +3. **Signed message is wrapped** in an internal message BoC and sent to the resource server as an x402 payment payload. +4. **Facilitator extracts** the signed body and wraps it in a new internal message from its own funded wallet (carrying TON for gas) and submits to the user's W5 wallet contract. 5. **W5 contract verifies** the Ed25519 signature and executes the Jetton transfer. The facilitator IS the relay — no third-party relay service is needed. Gas cost (~0.013 TON per transaction) is absorbed by the facilitator. From f2e138076dcbe220bd036d2645ba986395a40efd Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:48:19 +0900 Subject: [PATCH 13/19] spec: address skywardboundd review -- account states, asset clarification, simulation chain - Replace seqno==0 with proper TON account states (nonexist/uninit) for stateInit condition - Clarify that payload.asset is a JSON field, not a BoC-internal field (TEP-74 has no asset) - Add full TEP-74 transfer chain verification to simulation section - Remove non-normative timing note from settlement step 7 - Add W5 gasless transactions reference link --- specs/schemes/exact/scheme_exact_ton.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index 9520440502..4d21a2f66d 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -30,7 +30,7 @@ There is no relay commission. The facilitator absorbs gas costs as the cost of o 3. **Client** queries a TON RPC endpoint to resolve its Jetton wallet address (`get_wallet_address` on the Jetton master contract) and fetches its current wallet seqno. 4. **Client** constructs a `jetton_transfer` body ([TEP-74]) and wraps it in a W5 `internal_signed` message. 5. **Client** signs the message with their Ed25519 private key. -6. **Client** wraps the signed body in a bounceable internal message BOC (dest = client wallet, value = 0, with `stateInit` if `seqno == 0`) and base64-encodes it. +6. **Client** wraps the signed body in a bounceable internal message BOC (dest = client wallet, value = 0, with `stateInit` if the wallet account is not yet deployed, i.e. [`nonexist` or `uninit`](https://docs.ton.org/foundations/status)) and base64-encodes it. 7. **Client** sends a second request to the **Resource Server** with the `PaymentPayload`. 8. **Resource Server** forwards the payload and requirements to the **Facilitator's** `/verify` endpoint. 9. **Facilitator** deserializes the internal message BoC, derives the sender address and public key, verifies the Ed25519 signature, validates payment intent (amount, destination, asset), and checks replay protection (seqno, validUntil, BoC hash). @@ -146,9 +146,9 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks - `payload.settlementBoc` MUST decode as a valid TON internal message. - The `dest` field of the internal message is the client's wallet address (the sender/payer). - The message body MUST contain a valid W5 (v5r1+) signed transfer with opcode `0x73696e74` (`internal_signed`). -- The Ed25519 public key MUST be derived from the BoC's `stateInit` data cell (when `stateInit` is present, i.e. seqno == 0) or via the on-chain `get_public_key` getter on the client's wallet contract (when seqno > 0). The public key is NOT passed as a separate payload field. +- The Ed25519 public key MUST be derived from the BoC's `stateInit` data cell (when `stateInit` is present, i.e. the wallet account is [`nonexist` or `uninit`](https://docs.ton.org/foundations/status)) or via the on-chain `get_public_key` getter on the client's wallet contract (when the account is `active`). The public key is NOT passed as a separate payload field. - The Ed25519 signature MUST verify against the derived public key. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). -- If the message includes `stateInit` (seqno == 0), the facilitator MUST verify the contract code matches a known W5 wallet contract. The canonical code hash for W5R1 is `20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f`. Implementations SHOULD maintain an allowlist of accepted wallet code hashes and update it when TON Foundation publishes new wallet versions. Currently the allowlist contains one entry (W5R1). Wallet versions ship very rarely on TON (years apart), so this list is near-static. +- If the message includes `stateInit` (wallet is not yet deployed), the facilitator MUST verify the contract code matches a known W5 wallet contract. The canonical code hash for W5R1 is `20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f`. Implementations SHOULD maintain an allowlist of accepted wallet code hashes and update it when TON Foundation publishes new wallet versions. Currently the allowlist contains one entry (W5R1). Wallet versions ship very rarely on TON (years apart), so this list is near-static. - For wallets with seqno > 0, simulation (section 6) provides equivalent protection: if the contract does not execute the expected Jetton transfer, simulation will fail. Implementations that simulate MAY skip explicit code hash checks. ### 3. Facilitator safety @@ -159,8 +159,8 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks - The W5 message MUST contain exactly **1** `jetton_transfer` (opcode `0xf8a7ea5`) internal message. No additional actions are permitted. - The transfer amount MUST be equal to `requirements.amount`. -- The `asset` field in the payload MUST match `requirements.asset`. -- The source Jetton wallet (the destination of the W5 internal message in the BoC) MUST match the Jetton wallet address returned by `get_wallet_address(sender)` on the Jetton master contract (`requirements.asset`). This prevents a malicious BoC from using a substitute source contract. +- The Jetton master address (`payload.asset`) MUST equal `requirements.asset`. Note: [TEP-74] `jetton_transfer` does not carry the master contract address in its body, so the on-chain asset binding is verified in the next check. +- The source Jetton wallet (the destination of the W5 internal message in the BoC) MUST match the Jetton wallet address returned by `get_wallet_address(sender)` on the Jetton master contract (`requirements.asset`). This binds the BoC to the correct asset on-chain and prevents a malicious BoC from using a substitute Jetton wallet. - The `destination` field inside the `jetton_transfer` body MUST equal `requirements.payTo`. This ensures funds are routed to the correct recipient. ### 5. Replay protection @@ -174,8 +174,8 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks ### 6. Transaction simulation (recommended) -- Facilitator SHOULD simulate message execution via emulation during `/verify`. This protects the resource server from doing unnecessary work for invalid payloads. -- Verification SHOULD fail if simulation indicates: insufficient Jetton balance, expired message, or invalid seqno. +- Facilitator SHOULD simulate message execution via emulation during `/verify`. Simulation traces the full [TEP-74 Jetton transfer chain](https://docs.ton.org/standard/tokens/jettons/how-it-works#token-transfer) (transfer -> internal_transfer) and confirms the transfer reaches the recipient's Jetton wallet. This protects the resource server from doing unnecessary work for invalid payloads. +- Verification SHOULD fail if simulation indicates: insufficient Jetton balance, expired message, invalid seqno, or incomplete transfer chain. - When simulation is performed, it implicitly covers seqno, balance, and code hash checks from sections 2 and 5. ## Settlement Logic @@ -184,9 +184,9 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks 2. Extract the signed body and optional `stateInit` from the internal message BoC. 3. Fetch the facilitator's own wallet seqno. 4. Estimate gas via emulation: build a trial relay message, emulate the trace, and sum all fees across the trace. -5. Build the relay message: wrap the user's signed body in a bounceable internal message from the facilitator's wallet to the user's wallet, attaching the estimated TON for gas. If the user's wallet has a `stateInit` (seqno == 0), include it in the relay message for deployment. +5. Build the relay message: wrap the user's signed body in a bounceable internal message from the facilitator's wallet to the user's wallet, attaching the estimated TON for gas. If the client's BoC includes a `stateInit` (the wallet account is [`nonexist` or `uninit`](https://docs.ton.org/foundations/status)), include it in the relay message for wallet deployment. 6. Sign and broadcast the facilitator's external message. -7. Wait for transaction confirmation (typically < 5 seconds on TON). +7. Wait for transaction confirmation. 8. Return x402 `SettlementResponse` with `success`, `transaction`, `network`, and `payer`. ## Duplicate Settlement Mitigation (RECOMMENDED) @@ -273,6 +273,7 @@ TON uses the [TEP-74 Jetton standard][TEP-74] for fungible tokens: - [x402 v2 core specification](../../x402-specification-v2.md) - [TEP-74 Jetton Standard][TEP-74] - [W5 Wallet Contract](https://github.com/ton-blockchain/wallet-contract-v5) +- [W5 Gasless Transactions](https://docs.ton.org/standard/wallets/v5#gasless-transactions) - [TVM CAIP-2 Namespace](https://namespaces.chainagnostic.org/tvm/caip2) - [Facilitator](https://github.com/ohld/x402-ton-facilitator) - [POC](https://github.com/ohld/x402-ton-poc) From d6609981e1ad96a94953696df9ba90f618fc4ae0 Mon Sep 17 00:00:00 2001 From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:57:57 +0900 Subject: [PATCH 14/19] spec: address remaining review comments from skywardboundd - Narrow wallet scope to W5 v5r1 (not v5r1+), only version that exists today - Remove redundant "x402 protocol-level requirement" sentence - Add get_jetton_data hint for decimal resolution - Add walletId to derived fields list - Rename section 2 to "Message and signature verification" - Move balance check from Replay protection to Payment intent - Remove redundant seqno explanation sentence --- specs/schemes/exact/scheme_exact_ton.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index 4d21a2f66d..c70a11c174 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -13,11 +13,11 @@ This spec uses [CAIP-2](https://namespaces.chainagnostic.org/tvm/caip2) identifi - `tvm:-3` — TON testnet > [!NOTE] -> **Scope:** This spec covers [TEP-74]-compliant Jetton transfers using **W5+ wallets** (v5r1 and later) with gasless relay (`areFeesSponsored: true`). Non-gasless flows (`external_signed`, native TON transfers) are planned for a follow-up spec extension. +> **Scope:** This spec covers [TEP-74]-compliant Jetton transfers using **W5 wallets** (v5r1) with gasless relay (`areFeesSponsored: true`). Non-gasless flows (`external_signed`, native TON transfers) are planned for a follow-up spec extension. ## Summary -The `exact` scheme on TON transfers a specific amount of a [TEP-74] Jetton from the client to the resource server using a W5 wallet signed message. +The `exact` scheme on TON transfers a specific amount of a [TEP-74] Jetton from the client to the resource server using a W5 (v5r1) wallet signed message. The facilitator IS the relay. It sponsors gas (~0.013 TON per transaction) by wrapping the client-signed message in an internal TON message from its own funded wallet. The client resolves signing data (seqno, Jetton wallet address) via a TON RPC endpoint, signs locally, and sends the result. The facilitator cannot modify the destination or amount; the client controls payment intent through Ed25519 signature. @@ -34,7 +34,7 @@ There is no relay commission. The facilitator absorbs gas costs as the cost of o 7. **Client** sends a second request to the **Resource Server** with the `PaymentPayload`. 8. **Resource Server** forwards the payload and requirements to the **Facilitator's** `/verify` endpoint. 9. **Facilitator** deserializes the internal message BoC, derives the sender address and public key, verifies the Ed25519 signature, validates payment intent (amount, destination, asset), and checks replay protection (seqno, validUntil, BoC hash). -10. **Facilitator** returns a `VerifyResponse`. Verification is **REQUIRED** — it protects the resource server from doing unnecessary work for invalid payloads. This is an x402 protocol-level requirement, not specific to TON. +10. **Facilitator** returns a `VerifyResponse`. Verification is **REQUIRED** — it protects the resource server from doing unnecessary work for invalid payloads. 11. **Resource Server**, upon successful verification, fulfills the client's request. 12. **Resource Server** calls the **Facilitator's** `/settle` endpoint. The facilitator MUST perform full verification independently and MUST NOT assume prior `/verify` results. 13. **Facilitator** settles the payment: extracts the signed body from the internal message BoC, wraps it in a new internal message from its own wallet attaching TON for gas (estimated via emulation), signs and broadcasts. The user's W5 wallet verifies the signature and executes the Jetton transfer. @@ -62,7 +62,7 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: - `asset`: [TEP-74] Jetton master contract address (raw format `workchain:hex`). - `payTo`: Recipient TON address (raw format). -- `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). +- `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). Decimals can be queried via `get_jetton_data` on the Jetton master contract. - `extra.areFeesSponsored`: Whether the facilitator sponsors gas fees. Currently always `true`; a non-sponsored flow will be added in a follow-up spec. ## PaymentPayload `payload` Field @@ -112,7 +112,7 @@ Full `PaymentPayload` object: The facilitator derives the following from the BoC: - **Sender address**: the `dest` field of the internal message (the client's wallet). - **Public key**: from the `stateInit` data cell (if present) or via the on-chain `get_public_key` getter. -- **Amount, destination, validUntil, seqno**: from the W5 signed body and its actions. +- **walletId, amount, destination, validUntil, seqno**: from the W5 signed body and its actions. ## `SettlementResponse` @@ -141,11 +141,11 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks - `payload.accepted.payTo` MUST equal `requirements.payTo`. - `payload.accepted.amount` MUST equal `requirements.amount` exactly. -### 2. Signature validity +### 2. Message and signature verification - `payload.settlementBoc` MUST decode as a valid TON internal message. - The `dest` field of the internal message is the client's wallet address (the sender/payer). -- The message body MUST contain a valid W5 (v5r1+) signed transfer with opcode `0x73696e74` (`internal_signed`). +- The message body MUST contain a valid W5 (v5r1) signed transfer with opcode `0x73696e74` (`internal_signed`). - The Ed25519 public key MUST be derived from the BoC's `stateInit` data cell (when `stateInit` is present, i.e. the wallet account is [`nonexist` or `uninit`](https://docs.ton.org/foundations/status)) or via the on-chain `get_public_key` getter on the client's wallet contract (when the account is `active`). The public key is NOT passed as a separate payload field. - The Ed25519 signature MUST verify against the derived public key. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). - If the message includes `stateInit` (wallet is not yet deployed), the facilitator MUST verify the contract code matches a known W5 wallet contract. The canonical code hash for W5R1 is `20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f`. Implementations SHOULD maintain an allowlist of accepted wallet code hashes and update it when TON Foundation publishes new wallet versions. Currently the allowlist contains one entry (W5R1). Wallet versions ship very rarely on TON (years apart), so this list is near-static. @@ -162,12 +162,12 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks - The Jetton master address (`payload.asset`) MUST equal `requirements.asset`. Note: [TEP-74] `jetton_transfer` does not carry the master contract address in its body, so the on-chain asset binding is verified in the next check. - The source Jetton wallet (the destination of the W5 internal message in the BoC) MUST match the Jetton wallet address returned by `get_wallet_address(sender)` on the Jetton master contract (`requirements.asset`). This binds the BoC to the correct asset on-chain and prevents a malicious BoC from using a substitute Jetton wallet. - The `destination` field inside the `jetton_transfer` body MUST equal `requirements.payTo`. This ensures funds are routed to the correct recipient. +- The client MUST have sufficient balance of the payment asset. ### 5. Replay protection - The `validUntil` timestamp in the W5 body MUST NOT be expired and MUST NOT be more than `maxTimeoutSeconds` in the future. -- The wallet's on-chain seqno MUST be checked: the seqno in the BoC MUST be strictly equal to the current on-chain seqno. On TON, wallet contracts reject messages where the seqno does not match exactly. -- The client MUST have sufficient balance of the payment asset. +- The wallet's on-chain seqno MUST be checked: the seqno in the BoC MUST be strictly equal to the current on-chain seqno. - Duplicate `settlementBoc` submissions MUST be rejected via BoC hash dedup (see [Duplicate Settlement Mitigation](#duplicate-settlement-mitigation-recommended)). > **Note:** Seqno, balance, and expiry checks MAY be satisfied implicitly via transaction simulation (section 6). The spec declares them as explicit requirements so that implementations that do not simulate still enforce these checks. From b93882a5003ea7668caf1f423f4e3250c93619a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D0=BA=D0=B0=D0=B4=D0=B8=D0=B9=20=D0=A1=D1=82?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0?= <77278914+ArkadiyStena@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:50:33 +0300 Subject: [PATCH 15/19] spec: clarify TON exact flow for optional jetton transfer params Refine the TON `exact` scheme spec to better describe how client-signed W5 jetton transfers are verified and relayed by the facilitator. This update expands the TON payment requirements and verification rules to account for optional jetton transfer parameters exposed through `extra`, and clarifies how those parameters are interpreted when they are omitted. Changes included: - add optional `extra.forwardPayload` and `extra.forwardTonAmount` to TON `PaymentRequirements` and define their effective defaults as zero-bit payload and `"0"` - add optional `extra.responseDestination` and define its effective default as `addr_none` - require facilitators to compare the effective values of `responseDestination`, `forwardPayload`, and `forwardTonAmount` between `accepted.extra` and `requirements.extra` - extend payment intent validation so the facilitator derives and checks `response_destination`, `forward_payload`, and `forward_ton_amount` from the signed W5 action - clarify that the facilitator-funded relay must cover both outer relay execution and the value expected by the client-signed inner transfer - clarify that the client-signed inner message must carry enough TON to fund payer-side execution from the payer jetton wallet to the source jetton wallet - specify that the outbound internal message from the client W5 wallet to the source jetton wallet must be bounceable - specify that the `settlementBoc` wrapper itself does not need to be bounceable - clean up terminology and consistency across the document: `BoC`, `zero-bit`, `forwardPayload`, `forwardTonAmount`, and derived field descriptions This is a spec clarification/update only. It does not introduce a new TON payment scheme, but makes the existing gas-sponsored W5 relay flow more precise. --- specs/schemes/exact/scheme_exact_ton.md | 95 +++++++++++++------------ 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index c70a11c174..c577383811 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -13,13 +13,13 @@ This spec uses [CAIP-2](https://namespaces.chainagnostic.org/tvm/caip2) identifi - `tvm:-3` — TON testnet > [!NOTE] -> **Scope:** This spec covers [TEP-74]-compliant Jetton transfers using **W5 wallets** (v5r1) with gasless relay (`areFeesSponsored: true`). Non-gasless flows (`external_signed`, native TON transfers) are planned for a follow-up spec extension. +> **Scope:** This spec covers [TEP-74](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md)-compliant Jetton transfers using **W5 wallets** (v5r1) with gasless relay (`areFeesSponsored: true`). Non-gasless flows (`external_signed`, native TON transfers) are planned for a follow-up spec extension. ## Summary -The `exact` scheme on TON transfers a specific amount of a [TEP-74] Jetton from the client to the resource server using a W5 (v5r1) wallet signed message. +The `exact` scheme on TON transfers a specific amount of a [TEP-74](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md) Jetton from the client to the resource server using a W5 (v5r1) wallet signed message. -The facilitator IS the relay. It sponsors gas (~0.013 TON per transaction) by wrapping the client-signed message in an internal TON message from its own funded wallet. The client resolves signing data (seqno, Jetton wallet address) via a TON RPC endpoint, signs locally, and sends the result. The facilitator cannot modify the destination or amount; the client controls payment intent through Ed25519 signature. +The facilitator IS the relay. It sponsors gas (~0.013 TON per transaction) by wrapping the client-signed message in an internal TON message from its own funded wallet. The client resolves signing data (seqno, Jetton wallet address) via a TON RPC endpoint, signs locally, and sends the result. The facilitator cannot modify the destination or amount; the client controls payment intent through Ed25519 signature. The facilitator also attaches enough TON to the relay message to cover payer-wallet execution of the relayed message and to fund the client-signed inner transfer. There is no relay commission. The facilitator absorbs gas costs as the cost of operating the payment network, analogous to how EVM facilitators pay gas for `transferWithAuthorization`. @@ -28,17 +28,16 @@ There is no relay commission. The facilitator absorbs gas costs as the cost of o 1. **Client** requests a protected resource from the **Resource Server**. 2. **Resource Server** responds with HTTP 402 and `PaymentRequired` data. The `accepts` array includes a TON payment option. 3. **Client** queries a TON RPC endpoint to resolve its Jetton wallet address (`get_wallet_address` on the Jetton master contract) and fetches its current wallet seqno. -4. **Client** constructs a `jetton_transfer` body ([TEP-74]) and wraps it in a W5 `internal_signed` message. +4. **Client** constructs a `jetton_transfer` body ([TEP-74](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md)) and wraps it in a W5 `internal_signed` message with [sending mode 1 (PAY_FEES_SEPARATELY)](https://docs.ton.org/foundations/messages/modes). The outbound internal message sent by the client's W5 wallet to the source Jetton wallet MUST be bounceable. 5. **Client** signs the message with their Ed25519 private key. -6. **Client** wraps the signed body in a bounceable internal message BOC (dest = client wallet, value = 0, with `stateInit` if the wallet account is not yet deployed, i.e. [`nonexist` or `uninit`](https://docs.ton.org/foundations/status)) and base64-encodes it. +6. **Client** wraps the signed body in an internal message BoC (dest = client wallet, value = 0, with `stateInit` if the wallet account is not yet deployed, i.e. [`nonexist` or `uninit`](https://docs.ton.org/foundations/status)) and base64-encodes it. 7. **Client** sends a second request to the **Resource Server** with the `PaymentPayload`. 8. **Resource Server** forwards the payload and requirements to the **Facilitator's** `/verify` endpoint. 9. **Facilitator** deserializes the internal message BoC, derives the sender address and public key, verifies the Ed25519 signature, validates payment intent (amount, destination, asset), and checks replay protection (seqno, validUntil, BoC hash). 10. **Facilitator** returns a `VerifyResponse`. Verification is **REQUIRED** — it protects the resource server from doing unnecessary work for invalid payloads. -11. **Resource Server**, upon successful verification, fulfills the client's request. -12. **Resource Server** calls the **Facilitator's** `/settle` endpoint. The facilitator MUST perform full verification independently and MUST NOT assume prior `/verify` results. -13. **Facilitator** settles the payment: extracts the signed body from the internal message BoC, wraps it in a new internal message from its own wallet attaching TON for gas (estimated via emulation), signs and broadcasts. The user's W5 wallet verifies the signature and executes the Jetton transfer. -14. **Resource Server** returns the final response to the **Client**. +11. **Resource Server** calls the **Facilitator's** `/settle` endpoint. The facilitator MUST perform full verification independently and MUST NOT assume prior `/verify` results. +12. **Facilitator** settles the payment: it extracts the signed body from the internal message BoC, wraps it in a new internal message from its own wallet, attaching TON for gas (estimated via emulation), then signs and broadcasts it. The user's W5 wallet verifies the signature and executes the Jetton transfer. +13. **Resource Server** returns the final response to the **Client**. ## `PaymentRequirements` for `exact` @@ -53,6 +52,9 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: "payTo": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", "maxTimeoutSeconds": 300, "extra": { + "forwardPayload": "te6cckEBAgEAkwABnYgBFpKiX...", + "forwardTonAmount": "50000000", + "responseDestination": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", "areFeesSponsored": true } } @@ -60,14 +62,17 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: **Field Definitions:** -- `asset`: [TEP-74] Jetton master contract address (raw format `workchain:hex`). +- `asset`: [TEP-74](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md) Jetton master contract address (raw format `workchain:hex`). - `payTo`: Recipient TON address (raw format). - `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01). Decimals can be queried via `get_jetton_data` on the Jetton master contract. +- `extra.responseDestination` (optional): TON address used as `response_destination` in the `jetton_transfer` body. If omitted, the effective value is `addr_none` (i.e. 2 zero bits). +- `extra.forwardPayload` (optional): Base64-encoded cell used as the jetton transfer forward payload (see [TEP-74](https://github.com/ton-blockchain/TEPs/blob/63fc78718dd9930f3e106954ebec743c3ad07993/text/0074-jettons-standard.md?plain=1#L68)). If omitted, the effective value is a zero-bit cell. +- `extra.forwardTonAmount` (optional): the amount of nanotons to be attached to the jetton transfer (see [TEP-74](https://github.com/ton-blockchain/TEPs/blob/63fc78718dd9930f3e106954ebec743c3ad07993/text/0074-jettons-standard.md?plain=1#L68)). If omitted, the effective value is `"0"`. - `extra.areFeesSponsored`: Whether the facilitator sponsors gas fees. Currently always `true`; a non-sponsored flow will be added in a follow-up spec. ## PaymentPayload `payload` Field -The payload contains only the signed settlement BoC and the asset identifier. All other fields (sender address, amount, destination, public key) are derived from the BoC by the facilitator: +The payload contains only the signed settlement BoC and the asset identifier. All other fields (sender address, amount, destination, public key, `responseDestination`, `forwardPayload`, and `forwardTonAmount`) are derived from the BoC by the facilitator: ```json { @@ -94,6 +99,9 @@ Full `PaymentPayload` object: "payTo": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", "maxTimeoutSeconds": 300, "extra": { + "forwardPayload": "te6cckEBAgEAkwABnYgBFpKiX...", + "forwardTonAmount": "50000000", + "responseDestination": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed", "areFeesSponsored": true } }, @@ -106,13 +114,14 @@ Full `PaymentPayload` object: **Field Definitions:** -- `settlementBoc`: Base64-encoded TON internal message BoC. The internal message carries the signed W5 body as the `body` field, the client's wallet as `dest`, and optionally a `stateInit` for first-time wallet deployment. The message MUST be bounceable. +- `settlementBoc`: Base64-encoded TON internal message BoC. The internal message carries the signed W5 body as the `body` field, the client's wallet as `dest`, and optionally a `stateInit` for first-time wallet deployment. The `settlementBoc` wrapper itself does not need to be bounceable. The outbound internal message sent by the client's W5 wallet to the source Jetton wallet MUST be bounceable. - `asset`: Jetton master contract address in raw format. Kept as an explicit field for future multi-asset protocol extensions. The facilitator derives the following from the BoC: + - **Sender address**: the `dest` field of the internal message (the client's wallet). - **Public key**: from the `stateInit` data cell (if present) or via the on-chain `get_public_key` getter. -- **walletId, amount, destination, validUntil, seqno**: from the W5 signed body and its actions. +- **walletId, amount, destination, responseDestination, validUntil, seqno, forwardTonAmount, and forwardPayload**: from the W5 signed body and its actions. If `responseDestination`, `forwardTonAmount`, or `forwardPayload` are omitted from `accepted.extra`, the facilitator compares against the effective defaults (`addr_none`, `"0"`, and zero-bit payload, respectively). ## `SettlementResponse` @@ -140,6 +149,7 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks - `payload.accepted.asset` MUST equal `requirements.asset`. - `payload.accepted.payTo` MUST equal `requirements.payTo`. - `payload.accepted.amount` MUST equal `requirements.amount` exactly. +- The effective values of `responseDestination`, `forwardPayload`, and `forwardTonAmount` in `payload.accepted.extra` MUST match the corresponding effective values in `requirements.extra`. ### 2. Message and signature verification @@ -149,45 +159,40 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks - The Ed25519 public key MUST be derived from the BoC's `stateInit` data cell (when `stateInit` is present, i.e. the wallet account is [`nonexist` or `uninit`](https://docs.ton.org/foundations/status)) or via the on-chain `get_public_key` getter on the client's wallet contract (when the account is `active`). The public key is NOT passed as a separate payload field. - The Ed25519 signature MUST verify against the derived public key. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). - If the message includes `stateInit` (wallet is not yet deployed), the facilitator MUST verify the contract code matches a known W5 wallet contract. The canonical code hash for W5R1 is `20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f`. Implementations SHOULD maintain an allowlist of accepted wallet code hashes and update it when TON Foundation publishes new wallet versions. Currently the allowlist contains one entry (W5R1). Wallet versions ship very rarely on TON (years apart), so this list is near-static. -- For wallets with seqno > 0, simulation (section 6) provides equivalent protection: if the contract does not execute the expected Jetton transfer, simulation will fail. Implementations that simulate MAY skip explicit code hash checks. - -### 3. Facilitator safety -- The facilitator's own address MUST NOT appear as the sender (derived from the BoC's `dest` field) or as the source of any Jetton transfer. This prevents a malicious payload from tricking the facilitator into spending its own funds. +### 3. Payment intent -### 4. Payment intent - -- The W5 message MUST contain exactly **1** `jetton_transfer` (opcode `0xf8a7ea5`) internal message. No additional actions are permitted. +- The W5 message MUST contain exactly **1** `jetton_transfer` (opcode `0xf8a7ea5`) internal message with [sending mode 1 (PAY_FEES_SEPARATELY)](https://docs.ton.org/foundations/messages/modes). No additional actions are permitted. - The transfer amount MUST be equal to `requirements.amount`. -- The Jetton master address (`payload.asset`) MUST equal `requirements.asset`. Note: [TEP-74] `jetton_transfer` does not carry the master contract address in its body, so the on-chain asset binding is verified in the next check. +- The Jetton master address (`payload.asset`) MUST equal `requirements.asset`. Note: [TEP-74](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md) `jetton_transfer` does not carry the master contract address in its body, so the on-chain asset binding is verified in the next check. - The source Jetton wallet (the destination of the W5 internal message in the BoC) MUST match the Jetton wallet address returned by `get_wallet_address(sender)` on the Jetton master contract (`requirements.asset`). This binds the BoC to the correct asset on-chain and prevents a malicious BoC from using a substitute Jetton wallet. -- The `destination` field inside the `jetton_transfer` body MUST equal `requirements.payTo`. This ensures funds are routed to the correct recipient. +- The `destination`, `response_destination`, `forward_payload`, and `forward_ton_amount` parameters inside the `jetton_transfer` body MUST match the effective parameters in `requirements`: `requirements.payTo`, `requirements.extra.responseDestination ?? addr_none`, `requirements.extra.forwardPayload ?? zero-bit payload`, and `requirements.extra.forwardTonAmount ?? "0"`. +- The TON value carried by the user's signed internal message MUST be sufficient to cover execution starting from the payer wallet's outgoing message to the source Jetton wallet, including the effective `forward_ton_amount` (`requirements.extra.forwardTonAmount ?? "0"`) and downstream Jetton-wallet execution costs. Fees for delivering and executing the outer relay message on the payer wallet itself are sponsored separately by the facilitator and therefore do NOT need to be covered by the user's signed internal message. Facilitators SHOULD satisfy this check via emulation. - The client MUST have sufficient balance of the payment asset. -### 5. Replay protection +### 4. Replay protection - The `validUntil` timestamp in the W5 body MUST NOT be expired and MUST NOT be more than `maxTimeoutSeconds` in the future. - The wallet's on-chain seqno MUST be checked: the seqno in the BoC MUST be strictly equal to the current on-chain seqno. - Duplicate `settlementBoc` submissions MUST be rejected via BoC hash dedup (see [Duplicate Settlement Mitigation](#duplicate-settlement-mitigation-recommended)). -> **Note:** Seqno, balance, and expiry checks MAY be satisfied implicitly via transaction simulation (section 6). The spec declares them as explicit requirements so that implementations that do not simulate still enforce these checks. +> **Note:** Seqno, balance, and expiry checks MAY be satisfied implicitly via transaction simulation (section 5). The spec declares them as explicit requirements so that implementations that do not simulate still enforce these checks. -### 6. Transaction simulation (recommended) +### 5. Transaction simulation (recommended) - Facilitator SHOULD simulate message execution via emulation during `/verify`. Simulation traces the full [TEP-74 Jetton transfer chain](https://docs.ton.org/standard/tokens/jettons/how-it-works#token-transfer) (transfer -> internal_transfer) and confirms the transfer reaches the recipient's Jetton wallet. This protects the resource server from doing unnecessary work for invalid payloads. - Verification SHOULD fail if simulation indicates: insufficient Jetton balance, expired message, invalid seqno, or incomplete transfer chain. -- When simulation is performed, it implicitly covers seqno, balance, and code hash checks from sections 2 and 5. +- Simulation CAN replace the checks in section 3, but the checks from sections 1, 2, and 4 remain mandatory. ## Settlement Logic 1. Re-run all verification checks (do not trust prior `/verify` result). 2. Extract the signed body and optional `stateInit` from the internal message BoC. -3. Fetch the facilitator's own wallet seqno. -4. Estimate gas via emulation: build a trial relay message, emulate the trace, and sum all fees across the trace. -5. Build the relay message: wrap the user's signed body in a bounceable internal message from the facilitator's wallet to the user's wallet, attaching the estimated TON for gas. If the client's BoC includes a `stateInit` (the wallet account is [`nonexist` or `uninit`](https://docs.ton.org/foundations/status)), include it in the relay message for wallet deployment. -6. Sign and broadcast the facilitator's external message. -7. Wait for transaction confirmation. -8. Return x402 `SettlementResponse` with `success`, `transaction`, `network`, and `payer`. +3. Estimate gas via emulation: build a trial relay message, emulate the trace, and sum all fees across the trace. +4. Build the relay message: wrap the user's signed body in a bounceable internal message from the facilitator's wallet to the user's wallet, attaching sufficient TON to cover both (a) the outer relay delivery and payer-wallet execution fees and (b) the value expected by the client's signed internal message. If the client's BoC contains a `stateInit` segment (indicating the wallet is [`nonexist` or `uninit`](https://docs.ton.org/foundations/status)), include it in the relay message to enable wallet deployment. +5. Sign and broadcast the facilitator's external message. +6. Wait for transaction confirmation. +7. Return x402 `SettlementResponse` with `success`, `transaction`, `network`, and `payer`. ## Duplicate Settlement Mitigation (RECOMMENDED) @@ -228,12 +233,14 @@ The facilitator IS the relay — no third-party relay service is needed. Gas cos This is architecturally equivalent to x402's facilitator model on other chains: -| x402 Concept | TON Equivalent | EVM Equivalent | -|---|---|---| + +| x402 Concept | TON Equivalent | EVM Equivalent | +| ------------------------ | ----------------------------------------------- | --------------------------------------------- | | Facilitator sponsors gas | Facilitator sends internal message carrying TON | Facilitator calls `transferWithAuthorization` | -| Client signs offline | W5 `internal_signed` message | EIP-3009 authorization signature | -| Client RPC calls | seqno + `get_wallet_address` (2 calls) | nonce lookup (1 call, permit2 extension only) | -| Settlement | Facilitator wraps + broadcasts | Facilitator submits tx | +| Client signs offline | W5 `internal_signed` message | EIP-3009 authorization signature | +| Client RPC calls | seqno + `get_wallet_address` (2 calls) | nonce lookup (1 call, permit2 extension only) | +| Settlement | Facilitator wraps + broadcasts | Facilitator submits tx | + ### Client RPC Requirements @@ -256,7 +263,7 @@ Implementations MUST use raw format in protocol messages and MAY display friendl ### TEP-74 Jetton Standard -TON uses the [TEP-74 Jetton standard][TEP-74] for fungible tokens: +TON uses the [TEP-74 Jetton standard](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md) for fungible tokens: - Transfer opcode: `0xf8a7ea5` (`jetton_transfer`). - Each holder has a separate Jetton wallet contract. @@ -264,18 +271,18 @@ TON uses the [TEP-74 Jetton standard][TEP-74] for fungible tokens: ### Default Assets -| Network | Asset | Symbol | Decimals | Address | -|---|---|---|---|---| -| `tvm:-239` | USDT | USD₮ | 6 | `0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe` | + +| Network | Asset | Symbol | Decimals | Address | +| ---------- | ----- | ------ | -------- | -------------------------------------------------------------------- | +| `tvm:-239` | USDT | USD₮ | 6 | `0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe` | + ### References - [x402 v2 core specification](../../x402-specification-v2.md) -- [TEP-74 Jetton Standard][TEP-74] +- [TEP-74 Jetton Standard](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md) - [W5 Wallet Contract](https://github.com/ton-blockchain/wallet-contract-v5) - [W5 Gasless Transactions](https://docs.ton.org/standard/wallets/v5#gasless-transactions) - [TVM CAIP-2 Namespace](https://namespaces.chainagnostic.org/tvm/caip2) - [Facilitator](https://github.com/ohld/x402-ton-facilitator) - [POC](https://github.com/ohld/x402-ton-poc) - -[TEP-74]: https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md From c0863ceb820952f6c3789418d3dd6607a058f7fe Mon Sep 17 00:00:00 2001 From: Arkadiy Stena Date: Fri, 3 Apr 2026 14:52:57 +0300 Subject: [PATCH 16/19] specs(exact/ton): clarify W5 account states and extra field semantics --- specs/schemes/exact/scheme_exact_ton.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index c577383811..f66edd2d66 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -14,6 +14,8 @@ This spec uses [CAIP-2](https://namespaces.chainagnostic.org/tvm/caip2) identifi > [!NOTE] > **Scope:** This spec covers [TEP-74](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md)-compliant Jetton transfers using **W5 wallets** (v5r1) with gasless relay (`areFeesSponsored: true`). Non-gasless flows (`external_signed`, native TON transfers) are planned for a follow-up spec extension. +> +> For clients, only W5 wallets in account states `active` and `uninit`/`nonexist` are supported. `frozen` accounts are not supported. In practice this is not expected to constrain implementations because `frozen` W5 accounts are not expected to appear in the next 5 years. ## Summary @@ -70,6 +72,8 @@ In addition to standard x402 fields, TON `exact` uses `extra` fields: - `extra.forwardTonAmount` (optional): the amount of nanotons to be attached to the jetton transfer (see [TEP-74](https://github.com/ton-blockchain/TEPs/blob/63fc78718dd9930f3e106954ebec743c3ad07993/text/0074-jettons-standard.md?plain=1#L68)). If omitted, the effective value is `"0"`. - `extra.areFeesSponsored`: Whether the facilitator sponsors gas fees. Currently always `true`; a non-sponsored flow will be added in a follow-up spec. +These `extra` fields exist for TON-specific payment semantics: `forwardPayload` lets the payment carry application-level metadata (for example an invoice ID, analogous to a memo in other networks), `forwardTonAmount` enables the recipient to route the payment into custom contracts that require TON to execute logic on receipt, and `responseDestination` specifies where unused TON is returned as Jetton-transfer `excesses` as the exact network fee cannot be known in advance. + ## PaymentPayload `payload` Field The payload contains only the signed settlement BoC and the asset identifier. All other fields (sender address, amount, destination, public key, `responseDestination`, `forwardPayload`, and `forwardTonAmount`) are derived from the BoC by the facilitator: @@ -156,6 +160,7 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks - `payload.settlementBoc` MUST decode as a valid TON internal message. - The `dest` field of the internal message is the client's wallet address (the sender/payer). - The message body MUST contain a valid W5 (v5r1) signed transfer with opcode `0x73696e74` (`internal_signed`). +- The client's wallet account state MUST be either [`active`, `nonexist`, or `uninit`](https://docs.ton.org/foundations/status). `frozen` accounts are out of scope and MUST be rejected. - The Ed25519 public key MUST be derived from the BoC's `stateInit` data cell (when `stateInit` is present, i.e. the wallet account is [`nonexist` or `uninit`](https://docs.ton.org/foundations/status)) or via the on-chain `get_public_key` getter on the client's wallet contract (when the account is `active`). The public key is NOT passed as a separate payload field. - The Ed25519 signature MUST verify against the derived public key. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). - If the message includes `stateInit` (wallet is not yet deployed), the facilitator MUST verify the contract code matches a known W5 wallet contract. The canonical code hash for W5R1 is `20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f`. Implementations SHOULD maintain an allowlist of accepted wallet code hashes and update it when TON Foundation publishes new wallet versions. Currently the allowlist contains one entry (W5R1). Wallet versions ship very rarely on TON (years apart), so this list is near-static. From d30ca7bb2c207e0687339892fed4482e0e1147e5 Mon Sep 17 00:00:00 2001 From: Arkadiy Stena Date: Fri, 3 Apr 2026 15:34:08 +0300 Subject: [PATCH 17/19] soec(exact/ton): clarify code hash check --- specs/schemes/exact/scheme_exact.md | 2 +- specs/schemes/exact/scheme_exact_ton.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/schemes/exact/scheme_exact.md b/specs/schemes/exact/scheme_exact.md index 53e8bf5be9..707ed277ca 100644 --- a/specs/schemes/exact/scheme_exact.md +++ b/specs/schemes/exact/scheme_exact.md @@ -32,9 +32,9 @@ While implementation details vary by network, facilitators MUST enforce security ### TON -- Self-relay safety: the facilitator address MUST NOT appear as the sender or source of any Jetton transfer. - Transfer correctness: exactly 1 `jetton_transfer` with destination equal to `payTo` and amount equal to `requirements.amount` exactly. - Signature validity: Ed25519 signature MUST verify against a public key derived from the BoC's `stateInit` (seqno == 0) or from the on-chain `get_public_key` getter (seqno > 0). Only `internal_signed` (0x73696e74) opcode is supported in the current gasless flow. +- Wallet code validity: contract code MUST match a known W5 wallet contract, using `stateInit` for `nonexist`/`uninit` wallets and on-chain code for `active` wallets. - Replay protection: seqno MUST be strictly equal to on-chain value; duplicate `settlementBoc` submissions rejected via BoC hash dedup. - Simulation verification: SHOULD simulate via emulation during `/verify` to confirm expected balance changes. diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index f66edd2d66..b7286c6286 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -163,7 +163,7 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks - The client's wallet account state MUST be either [`active`, `nonexist`, or `uninit`](https://docs.ton.org/foundations/status). `frozen` accounts are out of scope and MUST be rejected. - The Ed25519 public key MUST be derived from the BoC's `stateInit` data cell (when `stateInit` is present, i.e. the wallet account is [`nonexist` or `uninit`](https://docs.ton.org/foundations/status)) or via the on-chain `get_public_key` getter on the client's wallet contract (when the account is `active`). The public key is NOT passed as a separate payload field. - The Ed25519 signature MUST verify against the derived public key. The signature is located at the TAIL of the W5 message body (after `walletId`, `validUntil`, `seqno`, and actions). -- If the message includes `stateInit` (wallet is not yet deployed), the facilitator MUST verify the contract code matches a known W5 wallet contract. The canonical code hash for W5R1 is `20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f`. Implementations SHOULD maintain an allowlist of accepted wallet code hashes and update it when TON Foundation publishes new wallet versions. Currently the allowlist contains one entry (W5R1). Wallet versions ship very rarely on TON (years apart), so this list is near-static. +- The facilitator MUST verify the client's wallet contract code matches a known W5 wallet contract. For [`nonexist` or `uninit`](https://docs.ton.org/foundations/status) accounts, the code MUST be taken from the `stateInit` included in `payload.settlementBoc`. For `active` accounts, the code MUST be fetched from the blockchain for the client's wallet address. The canonical code hash for W5R1 is `20834b7b72b112147e1b2fb457b84e74d1a30f04f737d4f62a668e9552d2b72f`. Implementations SHOULD maintain an allowlist of accepted wallet code hashes. Currently the allowlist contains one entry (W5R1). Wallet versions ship very rarely on TON (years apart), so this list is near-static. ### 3. Payment intent From 967230dcc8abb41c1d9147a35815392dec930093 Mon Sep 17 00:00:00 2001 From: Arkadiy Stena Date: Mon, 6 Apr 2026 20:50:11 +0300 Subject: [PATCH 18/19] spec(exact/ton): update reference implementations --- specs/schemes/exact/scheme_exact_ton.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index b7286c6286..0c76d3b5ba 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -218,9 +218,8 @@ This approach requires no external storage or long-lived state — only an in-pr ## Reference Implementations -- **Facilitator**: [ohld/x402-ton-facilitator](https://github.com/ohld/x402-ton-facilitator) -- **POC**: [ohld/x402-ton-poc](https://github.com/ohld/x402-ton-poc) -- **SDK**: [coinbase/x402#1583](https://github.com/coinbase/x402/pull/1583) +- **POC**: [ArkadiyStena/ton-x402-demo](https://github.com/ArkadiyStena/ton-x402-demo) +- **SDK**: [x402-foundation/x402#1944](https://github.com/x402-foundation/x402/pull/1944) ## Appendix From 83261e280bbd90ece1bd6befa580c91c7aeaa04f Mon Sep 17 00:00:00 2001 From: Arkadiy Stena Date: Wed, 8 Apr 2026 01:22:38 +0300 Subject: [PATCH 19/19] spec(exact/ton): allow sending mode 3 --- specs/schemes/exact/scheme_exact_ton.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/schemes/exact/scheme_exact_ton.md b/specs/schemes/exact/scheme_exact_ton.md index 0c76d3b5ba..73b8a5e2f9 100644 --- a/specs/schemes/exact/scheme_exact_ton.md +++ b/specs/schemes/exact/scheme_exact_ton.md @@ -30,7 +30,7 @@ There is no relay commission. The facilitator absorbs gas costs as the cost of o 1. **Client** requests a protected resource from the **Resource Server**. 2. **Resource Server** responds with HTTP 402 and `PaymentRequired` data. The `accepts` array includes a TON payment option. 3. **Client** queries a TON RPC endpoint to resolve its Jetton wallet address (`get_wallet_address` on the Jetton master contract) and fetches its current wallet seqno. -4. **Client** constructs a `jetton_transfer` body ([TEP-74](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md)) and wraps it in a W5 `internal_signed` message with [sending mode 1 (PAY_FEES_SEPARATELY)](https://docs.ton.org/foundations/messages/modes). The outbound internal message sent by the client's W5 wallet to the source Jetton wallet MUST be bounceable. +4. **Client** constructs a `jetton_transfer` body ([TEP-74](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md)) and wraps it in a W5 `internal_signed` message with [sending mode 1 (`PAY_FEES_SEPARATELY`) or mode 3 (`PAY_FEES_SEPARATELY | IGNORE_ERRORS`)](https://docs.ton.org/foundations/messages/modes). The outbound internal message sent by the client's W5 wallet to the source Jetton wallet MUST be bounceable. 5. **Client** signs the message with their Ed25519 private key. 6. **Client** wraps the signed body in an internal message BoC (dest = client wallet, value = 0, with `stateInit` if the wallet account is not yet deployed, i.e. [`nonexist` or `uninit`](https://docs.ton.org/foundations/status)) and base64-encodes it. 7. **Client** sends a second request to the **Resource Server** with the `PaymentPayload`. @@ -167,7 +167,7 @@ A facilitator verifying `exact` on TON MUST enforce all of the following checks ### 3. Payment intent -- The W5 message MUST contain exactly **1** `jetton_transfer` (opcode `0xf8a7ea5`) internal message with [sending mode 1 (PAY_FEES_SEPARATELY)](https://docs.ton.org/foundations/messages/modes). No additional actions are permitted. +- The W5 message MUST contain exactly **1** `jetton_transfer` (opcode `0xf8a7ea5`) internal message with [sending mode 1 (`PAY_FEES_SEPARATELY`) or mode 3 (`PAY_FEES_SEPARATELY | IGNORE_ERRORS`)](https://docs.ton.org/foundations/messages/modes). No additional actions are permitted. - The transfer amount MUST be equal to `requirements.amount`. - The Jetton master address (`payload.asset`) MUST equal `requirements.asset`. Note: [TEP-74](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md) `jetton_transfer` does not carry the master contract address in its body, so the on-chain asset binding is verified in the next check. - The source Jetton wallet (the destination of the W5 internal message in the BoC) MUST match the Jetton wallet address returned by `get_wallet_address(sender)` on the Jetton master contract (`requirements.asset`). This binds the BoC to the correct asset on-chain and prevents a malicious BoC from using a substitute Jetton wallet.