Skip to content

feat: add TVM exact-payment mechanism to Python SDK#1944

Merged
phdargen merged 10 commits intox402-foundation:mainfrom
the-ton-tech:main
May 8, 2026
Merged

feat: add TVM exact-payment mechanism to Python SDK#1944
phdargen merged 10 commits intox402-foundation:mainfrom
the-ton-tech:main

Conversation

@ArkadiyStena
Copy link
Copy Markdown
Contributor

  • add TVM exact client, server, and facilitator implementations
  • add TVM signers, codecs, provider, and settlement helpers
  • add unit and integration coverage for TON testnet/mainnet flows
  • add Python examples and e2e wiring for TVM

@github-actions github-actions Bot added sdk Changes to core v2 packages examples Changes to examples python labels Apr 6, 2026
@phdargen phdargen self-assigned this Apr 11, 2026
@@ -0,0 +1,84 @@
"""Registration helpers for TVM exact payment schemes."""
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.

please remove the register helper functions in favor of builder pattern, see #1266

from ..signer import ClientTvmSigner
from ..trace_utils import (
parse_trace_transactions,
trace_transaction_balance_before,
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.

trace_transaction_balance_before seems unused

settlement=settlement,
return_transaction=True,
)
actual_inner = settlement.transfer.attached_ton_amount
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.

It is my understanding the client sets the gas fee for the inner message. Could we add a reasonable cap here such that a malicious client can't arbitrarily inflate it, similar to SVM?

@phdargen
Copy link
Copy Markdown
Collaborator

Thanks @ArkadiyStena, made a first pass through the code and this looks great!

Could you please add the remaining e2e tests for:

  • clients: httpx, requests
  • server: fastapi, flask

@phdargen
Copy link
Copy Markdown
Collaborator

Got some testnet TON for the facilitator from @testgiver_ton_bot

For USDT, I tried @testgiver_ton_usdt_bot but its not responsive. Could you please point me to a USDT faucet, so I can run the e2e tests and examples?

Please also put the faucet info for TON + USDT in the README

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 14, 2026

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

A member of the Team first needs to authorize it.

@ArkadiyStena
Copy link
Copy Markdown
Contributor Author

@phdargen Thank you for the comments — addressed everything in the latest commit.

You can get testnet USDT by following the link: ton://transfer/kQDNUDJC0iQvJoZp0ml-YteL1NtTXKphU03CTI5v4VtBhGYs?amount=49000000&bin=te6cckEBAQEAFgAAKClXdJkAAAAAAAAAAAAAAAAAmJaAhDUekg
изображение

I’ve added this link to the docs.

Comment thread python/x402/mechanisms/tvm/README.md Outdated

## Testnet funding

To fund a TVM payer wallet, request testnet TON from [@testgiver_ton_bot](https://t.me/testgiver_ton_bot) for fees. Then open the [testnet USDT transfer link](ton://transfer/kQDNUDJC0iQvJoZp0ml-YteL1NtTXKphU03CTI5v4VtBhGYs?amount=49000000&bin=te6cckEBAQEAFgAAKClXdJkAAAAAAAAAAAAAAAAAmJaAhDUekg) or scan the QR code below to obtain testnet USDT:
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.

Suggested change
To fund a TVM payer wallet, request testnet TON from [@testgiver_ton_bot](https://t.me/testgiver_ton_bot) for fees. Then open the [testnet USDT transfer link](ton://transfer/kQDNUDJC0iQvJoZp0ml-YteL1NtTXKphU03CTI5v4VtBhGYs?amount=49000000&bin=te6cckEBAQEAFgAAKClXdJkAAAAAAAAAAAAAAAAAmJaAhDUekg) or scan the QR code below to obtain testnet USDT:
To fund a TVM payer wallet, request testnet TON from [@testgiver_ton_bot](https://t.me/testgiver_ton_bot) for fees. Then open the [testnet USDT transfer link](https://app.tonkeeper.com/transfer/kQDNUDJC0iQvJoZp0ml-YteL1NtTXKphU03CTI5v4VtBhGYs?amount=49000000&bin=te6cckEBAQEAFgAAKClXdJkAAAAAAAAAAAAAAAAAmJaAhDUekg) or scan the QR code below with the Tonkeeper App to obtain testnet USDT:

@phdargen
Copy link
Copy Markdown
Collaborator

Thanks @ArkadiyStena, have funded test accounts now but still cant get the examples/e2e tests running.
The client fails with:

in _estimate_required_inner_value
    raise ValueError("Trace does not contain the expected source jetton wallet transaction")
ValueError: Trace does not contain the expected source jetton wallet transaction

@phdargen
Copy link
Copy Markdown
Collaborator

OK actually the USDT claim didn't work initially. I confirmed the claim tx in the Tonkeeper wallet and it it showed success but then the USDT never arrived.

Seems I had to make a random TON transfer first, only then the USDT claim actually works? Maybe the wallet wasnt deployed or sth?

@phdargen
Copy link
Copy Markdown
Collaborator

Now the facilitator fails with:

TVM Facilitator account: 0:faf7999299a29d3548ea76123bf02ba0fc6c4f2efd53e12cbaf004f2cb1a89a3
🚀 All Networks Facilitator listening on http://0.0.0.0:4022
   Supported networks: eip155:84532, solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1, tvm:-3

INFO:     Started server process [14037]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:4022 (Press CTRL+C to quit)
INFO:     127.0.0.1:56541 - "GET /supported HTTP/1.1" 200 OK
Before verify: x402_version=2 payload={'settlementBoc': 'te6ccgEBBgEA/AABYGIAb337jsEyEHjUiQVr5T/BAtFg6Bps7qP/K9naHNRrbWEAAAAAAAAAAAAAAAAAAQEBoXNpbnR////9aeYPsQAAAAKYm2W891OC/RCUAgYxITRJJj2USa20EJKC0QqhIoNnZ2FXVi5Ri5TabujMnKE84fZvGcqOT2UAg6riz4oiaLDAIAICCg7DyG0BAwQAAAHHYgAoNmDxaC2fySmmChL0S4dRzwVCSGpJe19SQ+btb0+BUR+noIAAAAAAAAAAAAAAAAAAD4p+pQAAAAAAAAAAID6IAUMU3pWeyBryjBLYYUj3QBVtMZzybv7DmPZ+L2soPKjUAwUAAUA=', 'asset': '0:f418a04cf196ebc959366844a6cdf53a6fd6fff1eadafc892f05210bba31593e'} accepted=PaymentRequirements(scheme='exact', network='tvm:-3', asset='0:f418a04cf196ebc959366844a6cdf53a6fd6fff1eadafc892f05210bba31593e', amount='1000', pay_to='0:a18a6f4acf640d7946096c30a47ba00ab698ce79377f61cc7b3f17b5941e546a', max_timeout_seconds=300, extra={'areFeesSponsored': True, 'forwardPayload': 'te6ccgEBAQEAAwAAAUA=', 'forwardTonAmount': '0'}) resource=ResourceInfo(url='http://localhost:4021/weather', description='Weather report', mime_type='application/json') extensions=None
Toncenter request failed: method=POST path=/api/emulate/v1/emulateTrace url=https://testnet.toncenter.com/api/emulate/v1/emulateTrace status=500 attempt=1/5 retryable=True body='{"error":"failed to emulate shard block: cannot run message on account inbound external message rejected by account FAF7999299A29D3548EA76123BF02BA0FC6C4F2EFD53E12CBAF004F2CB1A89A3 before smart-contract execution"}'
Toncenter request failed: method=POST path=/api/emulate/v1/emulateTrace url=https://testnet.toncenter.com/api/emulate/v1/emulateTrace status=500 attempt=2/5 retryable=True body='{"error":"failed to emulate shard block: cannot run message on account inbound external message rejected by account FAF7999299A29D3548EA76123BF02BA0FC6C4F2EFD53E12CBAF004F2CB1A89A3 before smart-contract execution"}'
Toncenter request failed: method=POST path=/api/emulate/v1/emulateTrace url=https://testnet.toncenter.com/api/emulate/v1/emulateTrace status=500 attempt=3/5 retryable=True body='{"error":"failed to emulate shard block: cannot run message on account inbound external message rejected by account FAF7999299A29D3548EA76123BF02BA0FC6C4F2EFD53E12CBAF004F2CB1A89A3 before smart-contract execution"}'
Toncenter request failed: method=POST path=/api/emulate/v1/emulateTrace url=https://testnet.toncenter.com/api/emulate/v1/emulateTrace status=500 attempt=4/5 retryable=True body='{"error":"failed to emulate shard block: cannot run message on account inbound external message rejected by account FAF7999299A29D3548EA76123BF02BA0FC6C4F2EFD53E12CBAF004F2CB1A89A3 before smart-contract execution"}'
Toncenter request failed: method=POST path=/api/emulate/v1/emulateTrace url=https://testnet.toncenter.com/api/emulate/v1/emulateTrace status=500 attempt=5/5 retryable=True body='{"error":"failed to emulate shard block: cannot run message on account inbound external message rejected by account FAF7999299A29D3548EA76123BF02BA0FC6C4F2EFD53E12CBAF004F2CB1A89A3 before smart-contract execution"}'

Anything else missing I need to setup?

@ArkadiyStena
Copy link
Copy Markdown
Contributor Author

@phdargen Thanks for the feedback! I've updated the README to clarify that the facilitator contract needs a minimum balance of 1.1 TON before running the tests. I've also sent 2 TON to the facilitator address you mentioned in your previous message.

As for the USDT claim — it should work correctly. I just re-tested it on my end and the claim went through as expected. Maybe you've faced a temporary bug in the Tonkeeper. Please let me know if the issue persists.

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.

Had to increase timeout here:

Suggested change
httpx_timeout = float(os.getenv("HTTPX_TIMEOUT_SECONDS", "60"))
async with x402HttpxClient(client, timeout=httpx_timeout) as http:

or client fails with:

uv run python all_networks.py 
Initialized TVM account: 0:defbf71d826420f1a9120ad7ca7f8205a2c1d034d9dd47fe57b3b439a8d6dac2

Making request to: http://localhost:4021/weather

Toncenter request failed: method=POST path=/api/emulate/v1/emulateTrace url=https://testnet.toncenter.com/api/emulate/v1/emulateTrace status=429 attempt=1/5 retryable=True body='{"ok":false,"result":"Ratelimit exceed","code":429}'
Toncenter request failed: method=POST path=/api/emulate/v1/emulateTrace url=https://testnet.toncenter.com/api/emulate/v1/emulateTrace status=429 attempt=2/5 retryable=True body='{"ok":false,"result":"Ratelimit exceed","code":429}'
Traceback (most recent call last):
  File "//examples/python/clients/advanced/.venv/lib/python3.13/site-packages/httpx/_transports/default.py", line 101, in map_httpcore_exceptions
    yield
  File "//examples/python/clients/advanced/.venv/lib/python3.13/site-packages/httpx/_transports/default.py", line 394, in handle_async_request
    resp = await self._pool.handle_async_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "//examples/python/clients/advanced/.venv/lib/python3.13/site-packages/httpcore/_async/connection_pool.py", line 256, in handle_async_request
    raise exc from None
  File "//examples/python/clients/advanced/.venv/lib/python3.13/site-packages/httpcore/_async/connection_pool.py", line 236, in handle_async_request
    response = await connection.handle_async_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        pool_request.request
        ^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "//examples/python/clients/advanced/.venv/lib/python3.13/site-packages/httpcore/_async/connection.py", line 103, in handle_async_request
    return await self._connection.handle_async_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "//examples/python/clients/advanced/.venv/lib/python3.13/site-packages/httpcore/_async/http11.py", line 136, in handle_async_request
    raise exc
  File "//examples/python/clients/advanced/.venv/lib/python3.13/site-packages/httpcore/_async/http11.py", line 106, in handle_async_request
    ) = await self._receive_response_headers(**kwargs)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "//examples/python/clients/advanced/.venv/lib/python3.13/site-packages/httpcore/_async/http11.py", line 177, in _receive_response_headers
    event = await self._receive_event(timeout=timeout)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "//examples/python/clients/advanced/.venv/lib/python3.13/site-packages/httpcore/_async/http11.py", line 217, in _receive_event
    data = await self._network_stream.read(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        self.READ_NUM_BYTES, timeout=timeout
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "//examples/python/clients/advanced/.venv/lib/python3.13/site-packages/httpcore/_backends/anyio.py", line 32, in read
    with map_exceptions(exc_map):
         ~~~~~~~~~~~~~~^^^^^^^^^
  File "/opt/homebrew/Cellar/[email protected]/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/contextlib.py", line 162, in __exit__
    self.gen.throw(value)
    ~~~~~~~~~~~~~~^^^^^^^
  File "//examples/python/clients/advanced/.venv/lib/python3.13/site-packages/httpcore/_exceptions.py", line 14, in map_exceptions
    raise to_exc(exc) from exc
httpcore.ReadTimeout

while tx was actually successful

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.

Happened both with and without using TONCENTER API key

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.

e2e tests likely need similar fix

Copy link
Copy Markdown
Collaborator

@phdargen phdargen May 1, 2026

Choose a reason for hiding this comment

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

Still having the issue that I can't run the examples without increasing the httpx timeout @ArkadiyStena, which seems to be 5s by default

Running the example without TONCENTER API key takes about 26s
Running the example with TONCENTER API key (for both. facilitator and client) still takes about 18s

In contrast, doing an evm payment with the same example takes only ~3s, any ideas where this latency difference comes from? There is the settlement queue but this should only add ~1s

Copy link
Copy Markdown
Contributor Author

@ArkadiyStena ArkadiyStena May 1, 2026

Choose a reason for hiding this comment

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

Do I understand correctly that you’re encountering a 429 error even when using a Toncenter API key for testnet? Probably you’re running many tests in parallel with the same Toncenter API key?

Copy link
Copy Markdown
Collaborator

@phdargen phdargen May 1, 2026

Choose a reason for hiding this comment

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

I am running a single test with the advanced/all_networks examples.

I only get the 429 when running without an API key, but thats happens before the paid request when the client prepares the payload and is retried and eventually succeeds. So thats not the problem.

The issue is the http timeout because the paid request takes too long. With or without API key

Comment on lines +220 to +222
if self._settlement_cache.is_duplicate(
settlement.settlement_hash, requirements.max_timeout_seconds
):
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.

Was running some tests with concurrent request using the same payload and not sure if and for what the settlement cache is actually needed.

Seems when I remove this check, duplicate tx are still rejected with invalid_exact_tvm_payload_seqno_mismatch

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.

This is the scripts I used, similar to what I used to test: https://github.com/coinbase/x402/blob/main/specs/schemes/exact/scheme_exact_svm.md#duplicate-settlement-mitigation-recommended.

from __future__ import annotations

import asyncio
import json
import os
import sys
import time
from typing import Any

import httpx
from dotenv import load_dotenv

from x402 import prefer_network, x402Client
from x402.http import x402HTTPClient
from x402.mechanisms.tvm import (
    TVM_MAINNET,
    TVM_TESTNET,
    WalletV5R1Config,
    WalletV5R1MnemonicSigner,
)
from x402.mechanisms.tvm.exact import ExactTvmClientScheme

load_dotenv()


def _validate_environment() -> tuple[str, str, str, int]:
    """Return (tvm_private_key, base_url, endpoint_path, race_count) or exit."""
    tvm_private_key = os.getenv("TVM_PRIVATE_KEY")
    base_url = os.getenv("RESOURCE_SERVER_URL", "http://localhost:4021")
    endpoint_path = os.getenv("ENDPOINT_PATH", "/weather")
    race_raw = os.getenv("RACE_COUNT", "4")

    if not tvm_private_key:
        print("TVM_PRIVATE_KEY is required")
        sys.exit(1)

    try:
        race_count = int(race_raw)
    except ValueError:
        print("RACE_COUNT must be a positive integer")
        sys.exit(1)
    if race_count <= 0:
        print("RACE_COUNT must be a positive number")
        sys.exit(1)

    return tvm_private_key, base_url, endpoint_path, race_count


async def main() -> None:
    """Run the TVM race-condition demo: one payment, many parallel requests."""
    tvm_private_key, base_url, endpoint_path, race_count = _validate_environment()
    url = f"{base_url}{endpoint_path}"

    tvm_network = os.getenv("TVM_NETWORK", TVM_TESTNET)
    if tvm_network not in {TVM_TESTNET, TVM_MAINNET}:
        print(f"Unsupported TVM network: {tvm_network}")
        sys.exit(1)

    tvm_config = WalletV5R1Config.from_private_key(tvm_network, tvm_private_key)
    tvm_config.api_key = os.getenv("TONCENTER_API_KEY")
    tvm_config.base_url = os.getenv("TONCENTER_BASE_URL")
    tvm_signer = WalletV5R1MnemonicSigner(tvm_config)
    tvm_scheme = ExactTvmClientScheme(tvm_signer)

    print("TVM Race Condition Vulnerability Demo")
    print("=====================================")
    print(f"Server: {url}")
    print(f"Payer: {tvm_signer.address}")
    print(f"Race count: {race_count}")
    print("")

    client = x402Client()
    client.register_policy(prefer_network(tvm_network))
    client.register(tvm_network, tvm_scheme)
    http_client = x402HTTPClient(client)

    httpx_timeout = float(os.getenv("HTTPX_TIMEOUT_SECONDS", "60"))
    try:
        async with httpx.AsyncClient(timeout=httpx_timeout) as http:
            print("Getting 402 response...")
            initial = await http.get(url)
            if initial.status_code != 402:
                msg = f"Expected 402, got {initial.status_code}. Is the server running?"
                raise RuntimeError(msg)
            print("Got 402 Payment Required")

            print("Parsing payment requirements...")
            try:
                body: Any = initial.json()
            except json.JSONDecodeError as e:
                msg = "Failed to parse 402 response body as JSON"
                raise RuntimeError(msg) from e

            payment_required = http_client.get_payment_required_response(
                lambda name: initial.headers.get(name),
                body,
            )

            tvm_option = next(
                (
                    a
                    for a in payment_required.accepts
                    if str(a.network).startswith("tvm:") and a.scheme == "exact"
                ),
                None,
            )
            if tvm_option is None:
                msg = "No TVM exact payment option found in 402 response"
                raise RuntimeError(msg)

            print(f"Found TVM exact option on network: {tvm_option.network}")
            print(f"  Pay to: {tvm_option.pay_to}")
            print(f"  Amount: {tvm_option.amount}")
            print(f"  Asset:  {tvm_option.asset}")
            print("")

            print("Building and signing payment transaction once...")
            payment_payload = await client.create_payment_payload(payment_required)
            payment_headers = http_client.encode_payment_signature_header(payment_payload)

            print(f"Firing {race_count} parallel requests with the same payment header...")

            async def one_request(index: int) -> dict[str, Any]:
                r = await http.get(url, headers=payment_headers)
                try:
                    res_body: Any = r.json()
                except json.JSONDecodeError:
                    res_body = r.text
                payment_response: Any
                try:
                    payment_response = http_client.get_payment_settle_response(
                        r.headers.get
                    )
                except ValueError:
                    payment_response = None
                return {
                    "index": index,
                    "status": r.status_code,
                    "body": res_body,
                    "payment_response": payment_response,
                }

            start = time.perf_counter()
            results = await asyncio.gather(
                *(one_request(i) for i in range(race_count))
            )
            elapsed_ms = int((time.perf_counter() - start) * 1000)

        print("Results:")
        print("--------")

        succeeded = [r for r in results if r["status"] == 200]
        failed = [r for r in results if r["status"] != 200]

        for r in results:
            pr = r.get("payment_response")
            if pr is not None:
                pay_str = (
                    pr.model_dump_json()
                    if hasattr(pr, "model_dump_json")
                    else json.dumps(pr, default=str)
                )
                extra = f" | payment: {pay_str}"
            else:
                extra = ""
            print(
                f"  Request {r['index']}: HTTP {r['status']} - "
                f"{json.dumps(r['body'], default=str)}{extra}"
            )

        print("")
        print(f"Time: {elapsed_ms}ms")
        print("")

        if len(succeeded) > 1:
            print(
                f"VULNERABLE: {len(succeeded)}/{race_count} requests succeeded with the same payment."
            )
            print("The server accepted the same payment multiple times.")
        elif len(succeeded) == 1:
            print(
                f"PROTECTED: Only 1/{race_count} requests succeeded. "
                f"{len(failed)} were correctly rejected."
            )
        else:
            codes = ", ".join(str(r["status"]) for r in results)
            print(
                f"All {race_count} requests failed (status codes: {codes})."
            )
            print("Check that the server is running and accepting payments.")
    finally:
        tvm_scheme.close()


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except Exception as ex:
        print(str(ex), file=sys.stderr)
        sys.exit(1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey! Are you sure that the key you are using is valid and env variable is correctly set?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Was running some tests with concurrent request using the same payload and not sure if and for what the settlement cache is actually needed.

Seems when I remove this check, duplicate tx are still rejected with invalid_exact_tvm_payload_seqno_mismatch

The seqno changes only after the transaction is processed. If a new request with a duplicate settlement hash arrives before the previous one is processed on the blockchain, it will pass the seqno check. Now the risk of this is quite low, since transactions in the blockchain are processed in an average of 0.4 seconds, but nevertheless such a situation can happen.

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.

Ok thanks for the clarification!

Would it be sufficient to cache for 0.4 sec (or any reasonable fixed amount of a few secs to mins)?

MaxTimeout is a server defined field covering both api execution time + settlement and can be in principle arbitrarily large

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We've reworked it so the entry lifetime no longer depends on MaxTimeout in practice:

  • On any failure path (relay build/send, trace-confirmation timeout, per-item verifier rejection) the entry is released right away.
  • On success the entry is now also released as soon as the confirmation worker records the on-chain trace, because the W5 wallet's seqno has already advanced.

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.

OK thanks, thats sounds like a good solution

@up2itnow0822
Copy link
Copy Markdown
Contributor

Downstream client compatibility note from the paid MCP side:

PR #1944 is a useful signal for TVM/TON exact-payment adoption, especially because it exposes facilitator, account funding, jetton, gas, and settlement-cache behavior that EVM-only clients do not have to handle the same way.

I updated AgentPay MCP to fail closed for TVM requirements until that support is implemented deliberately:

  • restrict x402 signing to the configured AgentPay network instead of accepting a broad SDK default list,
  • reject CHAIN_ID=tvm:-3 with TVM-specific guidance,
  • return a clear unsupported-payment error when a 402 response only offers network: "tvm:-3",
  • avoid wallet signing or token transfer attempts for unsupported TVM/TON offers.

Proof:

This is not a request to change PR #1944. It is downstream proof that paid MCP clients should treat TVM as explicit support, not as another string in a generic network allowlist.


# Make request using async context manager
async with x402HttpxClient(client) as http:
async with x402HttpxClient(client, timeout=10.0) as http:
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.

Thanks for the update @ArkadiyStena, lets increase to 30s as requests without API key take ~20s

Suggested change
async with x402HttpxClient(client, timeout=10.0) as http:
async with x402HttpxClient(client, timeout=30.0) as http:

@phdargen
Copy link
Copy Markdown
Collaborator

phdargen commented May 7, 2026

Please increase then timeout in the client example to 30s and rebase against main to resolve merge conflicts @ArkadiyStena, then we can get this in

@github-actions github-actions Bot added ecosystem Additions to ecosystem site specs Spec changes or additions typescript go legacy Changes to legacy sdk or examples ci java labels May 8, 2026
@ArkadiyStena
Copy link
Copy Markdown
Contributor Author

ArkadiyStena commented May 8, 2026

Please increase then timeout in the client example to 30s and rebase against main to resolve merge conflicts @ArkadiyStena, then we can get this in

Done
Thanks

ArkadiyStena and others added 8 commits May 8, 2026 12:27
- add TVM exact client, server, and facilitator implementations
- add TVM signers, codecs, provider, and settlement helpers
- add unit and integration coverage for TON testnet/mainnet flows
- add Python examples and e2e wiring for TVM
Add TVM support to the Python e2e clients and servers, switch TVM usage to direct `ExactTvm*Scheme` registration after removing the old register helpers, and refresh docs/examples for testnet setup.
Also reject exact TVM settlements with excessively large attached TON amounts and cover the new validation with tests.
  On-chain seqno advances after a successful relay, so any retry of the same settlement BOC will fail at the W5 contract's seqno check regardless of the dedup reservation. Drop the entry as soon as the confirmation worker records success, so the invariant becomes "every is_duplicate=False is paired with a release" and TTL pruning is left as a crash safety-net only.
@phdargen
Copy link
Copy Markdown
Collaborator

phdargen commented May 8, 2026

Seems sth went wrong with the rebase @ArkadiyStena, there are still merge conflicts (probably due to changes in e2e tests in #2230 and #2109) and the PR shows changes in over 1000 files now

@github-actions github-actions Bot removed ecosystem Additions to ecosystem site specs Spec changes or additions typescript go legacy Changes to legacy sdk or examples ci java website docs labels May 8, 2026
Comment thread e2e/test.ts Outdated
}

// Load environment variables
config({ path: '.env-local' });
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.

please revert

@phdargen
Copy link
Copy Markdown
Collaborator

phdargen commented May 8, 2026

Thanks @ArkadiyStena, could you please fix the failing unit tests?

Copy link
Copy Markdown
Collaborator

@phdargen phdargen left a comment

Choose a reason for hiding this comment

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

LGTM, thanks @ArkadiyStena 🚀

@phdargen phdargen merged commit e0ba324 into x402-foundation:main May 8, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

examples Changes to examples python sdk Changes to core v2 packages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants