Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8754970
specs(exact): propose TON exact scheme for x402 v2 (spec-only)
ohld Mar 5, 2026
9f6683f
specs(exact/ton): address review feedback — exact amounts, commission…
ohld Mar 6, 2026
07cc680
Merge branch 'main' into feat/scheme-exact-ton
ohld Mar 10, 2026
a67ee39
Merge branch 'main' into feat/scheme-exact-ton
ohld Mar 12, 2026
bd4acad
Merge branch 'main' into feat/scheme-exact-ton
ohld Mar 12, 2026
a1f0435
Merge branch 'main' into feat/scheme-exact-ton
ohld Mar 13, 2026
cbad3e6
Merge branch 'main' into feat/scheme-exact-ton
ohld Mar 13, 2026
26df4c8
Merge branch 'main' into feat/scheme-exact-ton
ohld Mar 16, 2026
655fce5
specs(exact/ton): rewrite to self-relay architecture — address all re…
ohld Mar 16, 2026
754599b
Merge branch 'main' into feat/scheme-exact-ton
ohld Mar 17, 2026
c3d4c49
chore(spec): simplify TON spec — remove external_signed, condense ref…
ohld Mar 17, 2026
2796f26
fix(spec): remove 50% gas buffer — emulation is already worst-case
ohld Mar 17, 2026
78bf804
Merge branch 'main' into feat/scheme-exact-ton
ohld Mar 17, 2026
ace8bba
Merge branch 'main' into feat/scheme-exact-ton
ohld Mar 18, 2026
d532515
specs(exact/ton): address all review comments — remove /prepare, add RPC
ohld Mar 19, 2026
3e1b00f
specs(exact/ton): add facilitator safety, explicit jetton wallet check
ohld Mar 19, 2026
5b88a13
specs(exact/ton): add areFeesSponsored flag per Stellar pattern
ohld Mar 19, 2026
9678410
specs(exact/ton): fix validUntil duplication, add stateInit code hash…
ohld Mar 19, 2026
a294e62
Merge branch 'main' into feat/scheme-exact-ton
ohld Mar 19, 2026
aaae477
Merge branch 'main' into feat/scheme-exact-ton
ohld Mar 20, 2026
c569c24
specs(exact/ton): add payload.to and source Jetton wallet checks
ohld Mar 20, 2026
8c11af2
specs(exact/ton): remove facilitatorUrl, fix dedup TTL, add canonical…
ohld Mar 23, 2026
1d4cda7
specs(exact/ton): internal message BoC, minimal payload, strict seqno
ohld Mar 25, 2026
1d7fd98
Merge branch 'main' into feat/scheme-exact-ton
ohld Mar 25, 2026
f2e1380
spec: address skywardboundd review -- account states, asset clarifica…
ohld Mar 26, 2026
d660998
spec: address remaining review comments from skywardboundd
ohld Mar 26, 2026
c0b6340
Merge branch 'main' into feat/scheme-exact-ton
ohld Mar 26, 2026
b93882a
spec: clarify TON exact flow for optional jetton transfer params
ArkadiyStena Apr 2, 2026
c0863ce
specs(exact/ton): clarify W5 account states and extra field semantics
ArkadiyStena Apr 3, 2026
d30ca7b
soec(exact/ton): clarify code hash check
ArkadiyStena Apr 3, 2026
967230d
spec(exact/ton): update reference implementations
ArkadiyStena Apr 6, 2026
83261e2
spec(exact/ton): allow sending mode 3
ArkadiyStena Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion specs/schemes/exact/scheme_exact.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,12 @@ 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

- 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).
291 changes: 291 additions & 0 deletions specs/schemes/exact/scheme_exact_ton.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
# 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 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.

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`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Someone has to pay for this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The facilitator operator funds their own wallet, same model as EVM where the facilitator pays gas for transferWithAuthorization.


## 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`.
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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Message should be bouncable

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed, the internal message in settlementBoc MUST be bounceable now.

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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It’s not entirely clear why we wrap internal_body and state_init in an external message. I think it would be better to split settlementBoc into separate fields, such as internalBoc and stateInitBoc, since that would be easier to understand. If we want to keep a single field, then it would make more sense to encode it as an internal message rather than an external one, because the internal message is what the facilitator will actually send to the network.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Agree, I think Internal Message will work in this case. In includes all the useful data required for the future transaction processing

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point. We considered splitting into separate internalBoc + stateInitBoc fields, but went with encoding as a single internal message since it naturally carries both the body and stateInit in one structure. Updated in the latest commit: settlementBoc is now an internal message (bounceable, dest=client wallet, value=0). The facilitator extracts body + stateInit from 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.
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**.

## `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": {
"facilitatorUrl": "https://facilitator.example.com",
"areFeesSponsored": true
}
}
```

**Field Definitions:**

- `asset`: [TEP-74] Jetton master contract address (raw format `workchain:hex`).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Are there future plans for non-gasless TEP-74 transfers and native TON transfers? The spec is built only for gasless payments, with the required facilitator entity. In the future it'll be hard to extend this document with another payments. Maybe it is better to plan them ahead?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, we want to support non-gasless TEP-74 transfers and native TON transfers in follow-up specs. Added a note in the scope section. The current spec is intentionally focused on the gasless flow to keep the first version simple, but the internal message format we chose should make it straightforward to extend.

- `payTo`: Recipient TON address (raw format).
- `amount`: Atomic token amount (6 decimals for USDT, so `10000` = $0.01).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There is no comments how we can get decimal parameter

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added a note that decimals can be queried via get_jetton_data on the Jetton master contract.

- `extra.facilitatorUrl`: URL of the facilitator server. The resource server calls `{facilitatorUrl}/verify` and `{facilitatorUrl}/settle`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

facilitatorUrl is not needed, please remove

- `extra.areFeesSponsored`: Whether the facilitator sponsors gas fees. Currently always `true`; a non-sponsored flow will be added later.

## PaymentPayload `payload` Field

The `payload` field contains the signed message and metadata needed for verification and settlement:

```json
{
"from": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f",
"to": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed",
"tokenMaster": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe",
"amount": "10000",
"validUntil": 1772689900,
"settlementBoc": "te6cckEBAgEAkwABnYgBFpKiX...",
"walletPublicKey": "14f77792ea084b4defa9bf5e99335682dd556b8ddf1943dca052ca56276136a8"
}
```

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": {
"facilitatorUrl": "https://facilitator.example.com",
"areFeesSponsored": true
}
},
"payload": {
"from": "0:1da21a6e33ef22840029ae77900f61ba820b94e813a3b7bef4e3ea471007645f",
"to": "0:92433a576cbe56c4dcc86d94b497a2cf18a9baa9c8283fea28ea43eb3c25cfed",
"tokenMaster": "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe",
"amount": "10000",
"validUntil": 1772689900,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All of these fields can be derived from settlementBoc. If included explicitly, they must be additionally validated, which is a poor developer experience.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Except for the tokenMaster. This field maybe include for the future protocol extension with multiple accepted assets. But let's rename it to "assert" for clarity

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, renamed to asset in the payload. Matches requirements.asset and leaves room for multi-asset extensions.

"settlementBoc": "te6cckEBAgEAkwABnYgBFpKiX...",
"walletPublicKey": "14f77792ea084b4defa9bf5e99335682dd556b8ddf1943dca052ca56276136a8"
}
}
```

**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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In exact_scheme.md and in the facilitator reference implementation, support for external_signed messages is mentioned, whereas exact_scheme_ton.md only refers to internal_signed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Right, cleaned this up. The current spec now explicitly scopes to internal_signed only (gasless flow). Added a note at the top that non-gasless flows (external_signed, native TON) are planned for a follow-up extension.

- `walletPublicKey`: Ed25519 public key in hex, used for signature verification.

## `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).

## Facilitator Verification Rules (MUST)

A facilitator verifying `exact` on TON MUST enforce all of the following checks before sponsoring and relaying the transaction:

### 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` 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. Signature validity
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It isn't about client signature validity

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Renamed to "Message and signature verification".


- `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).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There should be a check that payload.walletPublicKey matches the public key actually obtained from the blockchain for the address payload.from, or from the wallet’s stateInit. With that approach, passing payload.walletPublicKey separately is generally unnecessary.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed, removed walletPublicKey from the payload entirely. The facilitator now derives the public key from the stateInit data cell (seqno == 0) or via the on-chain get_public_key getter (seqno > 0). Much cleaner.

- 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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What would be the size of the allowlist currently and how frequently do you anticipate this to change?

We should avoid scenarios where some clients can only use some facilitators based on an opaque allowlist the facilitator keeps internally. Might be worth to define a canonical set of code hashes that MUST be accepted.

For SVM, we want to move to simulation based verification instead of an explicit smart wallet whitelist, see
https://github.com/coinbase/x402/pull/1527. Could a similar approach work here?

I think for now we could live with an allowlist, but something to consider for future updates if feasible

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The allowlist currently has 1 entry: W5R1. I added the canonical code hash inline in the latest commit so implementations don't need to discover it externally.

TON wallet versions ship very rarely. v3 to v4 and v4 to v5 were years apart each, so in practice this list is near-static. When a new version does ship, it would be announced by TON Foundation and implementations would add the new hash.

Regarding simulation-based verification: yes, this works on TON. The emulation API runs the full transaction trace, so if the contract doesn't produce the expected jetton transfer, the output won't match and verification fails. I've noted this in the spec: implementations that simulate MAY skip explicit code hash checks. If SVM moves to simulation-only (PR #1527), we can follow the same direction here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The wallet contract check is only done for seqno == 0. Would it be possible to deploy a malicious wallet contract that could grief the facilitator for seqno>0 by eg keeping the TON but not executing the Jetton transfer? I suppose the simulation checks would fail then, just to double-check with you that there is no attack vector

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good question. Short answer: simulation catches this, and without simulation the risk is limited to gas (~$0.04).

The key property of W5R1 is that it has no setCode opcode. Once deployed, the contract code is immutable. So a wallet that was deployed as W5R1 at seqno=0 cannot become malicious later. cc @skywardboundd

The remaining scenario is a non-W5R1 contract that was never deployed via x402 (so no stateInit check happened). If such a contract accepts the facilitator's TON but doesn't execute the jetton transfer, simulation will show that the expected transfer output is missing and verification will fail before settlement.

Without simulation, the facilitator would lose gas (~0.013 TON / $0.04 per attempt), but the payment itself won't execute, so no funds are stolen. Economically irrational for the attacker since they gain nothing.

For extra safety, implementations can also query the on-chain code hash for any wallet address (not just seqno=0). But simulation is the cleaner path forward.


### 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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think that this makes no sense. How the payload would trick the facilitator if he will emulate before sending funds?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fair point. Updated the spec: implementations that simulate MAY skip explicit code hash checks (simulation catches any malicious contract behavior). The code hash check remains as a requirement only for implementations that don't simulate.


### 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`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Let add smth like that

  • The recipient Jetton wallet contract MUST match the payload.tokenMaster.get_wallet_address(payload.to).

- 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

- `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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On TON seqno should be strictly equal to stored. Is there any business requirements behind this (delayed send)? Or just a missprint?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Misprint on our side, thanks. Fixed to strict equality. TON wallets reject anything that doesn't match exactly.

- The client MUST have sufficient balance of the payment asset.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's not about replay protection

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You're right, moved the balance check to section 4 (Payment intent).

- 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.

### 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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added. Simulation now explicitly traces the full TEP-74 transfer chain (transfer -> internal_transfer).

- When simulation is performed, it implicitly covers seqno and balance checks from section 5.

## Settlement Logic

1. Re-run all verification checks (do not trust prior `/verify` result).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

By this is it meant that Resource Server should do the emulation as well? In this case why /verify is needed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The /verify step is an x402 protocol-level requirement, not TON-specific. It lets the resource server reject invalid payloads before doing any actual work (API execution, data retrieval, etc). The facilitator then re-verifies independently during /settle.

@phdargen would be great to hear your thoughts on this too. Is there a better way to frame the verify/settle separation in the spec for chains where simulation covers everything?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Facilitators should reverify cheap checks like payload fields in settle/, the may skip checks that require rpc calls like balance checks or simulation if they want to optimize for latency, at the risk of wasting gas for edge cases where tx become invalid between verify/ and settle/.

For EVM, we have a config flag to give facilitators the choice:

export interface EIP3009FacilitatorConfig {
/**
* If enabled, the facilitator will deploy ERC-4337 smart wallets
* via EIP-6492 when encountering undeployed contract signatures.
*
* @default false
*/
deployERC4337WithEIP6492: boolean;
/**
* If enabled, simulates transaction before settling. Defaults to false, ie only simulate during verify.
*
* @default false
*/
simulateInSettle?: boolean;
}

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, 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).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why we talk about time here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed.

8. 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why are we talking about this? If we want to protect ourselves from the vulnerability mentioned above, then either simulation or checking the seqno helps.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This covers a specific race condition: if the same BoC is submitted twice before the first settlement lands on-chain, both /verify calls pass because the seqno hasn't changed yet. The attacker gets 2 resource accesses for 1 payment. Seqno/simulation don't help here because on-chain state is unchanged between the two calls. The cache closes this window. It's RECOMMENDED, not MUST.


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 `maxTimeoutSeconds` from the corresponding `PaymentRequirements`. After this window, the signed message will have expired and cannot land on-chain regardless.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

maxTimeoutSeconds is a server-side setting that covers time for API execution + tx confirmation.

SVM caches around /settle for a fixed 2mins after which the blockhash will be expired.
Could we also defined a fixed time here after which the seqno should have safely advanced onchain?


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.

## 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)

## Appendix

### 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. **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 per transaction) is absorbed by the facilitator.

This is architecturally equivalent to x402's facilitator model on other chains:

| 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 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.
- **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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added, thanks!


- [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)
- [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
Loading