Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
56 changes: 56 additions & 0 deletions .github/workflows/publish_npm_scoped_x402_tvm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Publish @x402/tvm package to NPM

on:
workflow_dispatch:

jobs:
publish-npm-x402-tvm:
runs-on: ubuntu-latest
environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }}
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
with:
version: 10.7.0

- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
cache: "pnpm"
cache-dependency-path: ./typescript

- name: Update npm for OIDC trusted publishing
run: npm install -g npm@latest

- name: Configure npm for trusted publishing
run: npm config delete always-auth 2>/dev/null || true

- name: Install and build
working-directory: ./typescript
run: |
pnpm install --frozen-lockfile
pnpm -r --filter=@x402/core --filter=@x402/tvm run build

- name: Publish @x402/tvm package
working-directory: ./typescript/packages/mechanisms/tvm
run: |
# Get package information directly
PACKAGE_NAME=$(node -p "require('./package.json').name")
PACKAGE_VERSION=$(node -p "require('./package.json').version")

echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION"

# Check if running on main branch
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "Publishing to NPM (main branch)"
pnpm publish --provenance --access public
else
echo "Dry run only (non-main branch: ${{ github.ref }})"
pnpm publish --dry-run --no-git-checks
fi
18 changes: 15 additions & 3 deletions examples/typescript/clients/advanced/all_networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
* optional chain configuration via environment variables.
*
* New chain support should be added here in alphabetic order by network prefix
* (e.g., "eip155" before "solana" before "stellar").
* (e.g., "eip155" before "solana" before "stellar" before "tvm").
*/

import { config } from "dotenv";
import { x402Client, wrapFetchWithPayment, x402HTTPClient } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { ExactSvmScheme } from "@x402/svm/exact/client";
import { ExactStellarScheme } from "@x402/stellar/exact/client";
import { ExactTvmScheme } from "@x402/tvm/exact/client";
import { createEd25519Signer } from "@x402/stellar";
import { toClientTvmSigner } from "@x402/tvm";
import { privateKeyToAccount } from "viem/accounts";
import { createKeyPairSignerFromBytes } from "@solana/kit";
import { mnemonicToPrivateKey } from "@ton/crypto";
import { base58 } from "@scure/base";

config();
Expand All @@ -24,6 +27,7 @@ config();
const evmPrivateKey = process.env.EVM_PRIVATE_KEY as `0x${string}` | undefined;
const svmPrivateKey = process.env.SVM_PRIVATE_KEY as string | undefined;
const stellarPrivateKey = process.env.STELLAR_PRIVATE_KEY as string | undefined;
const tvmMnemonic = process.env.TVM_MNEMONIC as string | undefined;
const baseURL = process.env.RESOURCE_SERVER_URL || "http://localhost:4021";
const endpointPath = process.env.ENDPOINT_PATH || "/weather";
const url = `${baseURL}${endpointPath}`;
Expand All @@ -34,9 +38,9 @@ const url = `${baseURL}${endpointPath}`;
*/
async function main(): Promise<void> {
// Validate at least one private key is provided
if (!evmPrivateKey && !svmPrivateKey && !stellarPrivateKey) {
if (!evmPrivateKey && !svmPrivateKey && !stellarPrivateKey && !tvmMnemonic) {
console.error(
"❌ At least one of EVM_PRIVATE_KEY, SVM_PRIVATE_KEY, or STELLAR_PRIVATE_KEY is required",
"❌ At least one of EVM_PRIVATE_KEY, SVM_PRIVATE_KEY, STELLAR_PRIVATE_KEY, or TVM_MNEMONIC is required",
);
process.exit(1);
}
Expand Down Expand Up @@ -65,6 +69,14 @@ async function main(): Promise<void> {
console.log(`Initialized Stellar account: ${stellarSigner.address}`);
}

// Register TVM scheme if mnemonic is provided
if (tvmMnemonic) {
const keyPair = await mnemonicToPrivateKey(tvmMnemonic.split(" "));
const tvmSigner = toClientTvmSigner(keyPair);
client.register("tvm:*", new ExactTvmScheme(tvmSigner));
console.log(`Initialized TVM account: ${tvmSigner.address}`);
}

// Wrap fetch with payment handling
const fetchWithPayment = wrapFetchWithPayment(fetch, client);

Expand Down
2 changes: 2 additions & 0 deletions examples/typescript/clients/advanced/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"@x402/evm": "workspace:*",
"@x402/svm": "workspace:*",
"@x402/stellar": "workspace:*",
"@x402/tvm": "workspace:*",
"@x402/fetch": "workspace:*",
"@ton/crypto": "^3.3.0",
"dotenv": "^16.4.7",
"viem": "^2.39.0",
"@solana/kit": "^6.1.0"
Expand Down
23 changes: 20 additions & 3 deletions examples/typescript/servers/advanced/all_networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* optional chain configuration via environment variables.
*
* New chain support should be added here in alphabetic order by network prefix
* (e.g., "eip155" before "solana" before "stellar").
* (e.g., "eip155" before "solana" before "stellar" before "tvm").
*/

import { config } from "dotenv";
Expand All @@ -14,6 +14,7 @@ import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { ExactSvmScheme } from "@x402/svm/exact/server";
import { ExactStellarScheme } from "@x402/stellar/exact/server";
import { ExactTvmScheme } from "@x402/tvm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";

config();
Expand All @@ -22,10 +23,11 @@ config();
const evmAddress = process.env.EVM_ADDRESS as `0x${string}` | undefined;
const svmAddress = process.env.SVM_ADDRESS as string | undefined;
const stellarAddress = process.env.STELLAR_ADDRESS as string | undefined;
const tvmAddress = process.env.TVM_ADDRESS as string | undefined;

// Validate at least one address is provided
if (!evmAddress && !svmAddress && !stellarAddress) {
console.error("❌ At least one of EVM_ADDRESS, SVM_ADDRESS, or STELLAR_ADDRESS is required");
if (!evmAddress && !svmAddress && !stellarAddress && !tvmAddress) {
console.error("❌ At least one of EVM_ADDRESS, SVM_ADDRESS, STELLAR_ADDRESS, or TVM_ADDRESS is required");
process.exit(1);
}

Expand All @@ -39,6 +41,7 @@ if (!facilitatorUrl) {
const EVM_NETWORK = "eip155:84532" as const; // Base Sepolia
const SVM_NETWORK = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" as const; // Solana Devnet
const STELLAR_NETWORK = "stellar:testnet" as const; // Stellar Testnet
const TVM_NETWORK = "tvm:-239" as const; // TON Mainnet

// Build accepts array dynamically based on configured addresses
const accepts: Array<{
Expand Down Expand Up @@ -71,6 +74,14 @@ if (stellarAddress) {
payTo: stellarAddress,
});
}
if (tvmAddress) {
accepts.push({
scheme: "exact",
price: "$0.001",
network: TVM_NETWORK,
payTo: tvmAddress,
});
}

// Create facilitator client
const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl });
Expand All @@ -86,6 +97,9 @@ if (svmAddress) {
if (stellarAddress) {
server.register(STELLAR_NETWORK, new ExactStellarScheme());
}
if (tvmAddress) {
server.register(TVM_NETWORK, new ExactTvmScheme());
}

// Create Express app
const app = express();
Expand Down Expand Up @@ -132,6 +146,9 @@ app.listen(port, () => {
if (stellarAddress) {
console.log(` Stellar: ${stellarAddress} on ${STELLAR_NETWORK}`);
}
if (tvmAddress) {
console.log(` TVM: ${tvmAddress} on ${TVM_NETWORK}`);
}
console.log(` Facilitator: ${facilitatorUrl}`);
console.log();
});
1 change: 1 addition & 0 deletions examples/typescript/servers/advanced/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@x402/evm": "workspace:*",
"@x402/svm": "workspace:*",
"@x402/stellar": "workspace:*",
"@x402/tvm": "workspace:*",
"@x402/extensions": "workspace:*"
},
"devDependencies": {
Expand Down
5 changes: 5 additions & 0 deletions typescript/.changeset/add-tvm-mechanism.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@x402/tvm": minor
---

Added TVM (TON) mechanism for exact payment scheme with gasless USDT support.
5 changes: 0 additions & 5 deletions typescript/.changeset/support-express-style-route-params.md

This file was deleted.

5 changes: 2 additions & 3 deletions typescript/packages/core/src/http/x402HTTPResourceServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,7 @@ export class x402HTTPResourceServer {
/**
* Parse route pattern into verb and regex
*
* @param pattern - Route pattern like "GET /api/*", "/api/[id]", or "/api/:id"
* @param pattern - Route pattern like "GET /api/*" or "/api/[id]"
* @returns Parsed pattern with verb and regex
*/
private parseRoutePattern(pattern: string): { verb: string; regex: RegExp } {
Expand All @@ -888,8 +888,7 @@ export class x402HTTPResourceServer {
path
.replace(/[$()+.?^{|}]/g, "\\$&") // Escape regex special chars
.replace(/\*/g, ".*?") // Wildcards
.replace(/\[([^\]]+)\]/g, "[^/]+") // Parameters (Next.js style [param])
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "[^/]+") // Parameters (Express style :param)
.replace(/\[([^\]]+)\]/g, "[^/]+") // Parameters
.replace(/\//g, "\\/") // Escape slashes
}$`,
"i",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,84 +372,6 @@ describe("x402HTTPResourceServer", () => {
expect(result.type).toBe("payment-error"); // Route matched
});

it("should match Express-style :param dynamic routes", async () => {
const routes = {
"/api/chapters/:seriesId/:chapterId": {
accepts: {
scheme: "exact",
payTo: "0xabc",
price: "$1.00" as Price,
network: "eip155:8453" as Network,
},
},
};

const httpServer = new x402HTTPResourceServer(ResourceServer, routes);

const adapter = new MockHTTPAdapter();
const context: HTTPRequestContext = {
adapter,
path: "/api/chapters/abc123/chapter-7",
method: "GET",
};

const result = await httpServer.processHTTPRequest(context);

expect(result.type).toBe("payment-error"); // Route matched
});

it("should match Express-style :param with HTTP method prefix", async () => {
const routes = {
"GET /api/users/:id": {
accepts: {
scheme: "exact",
payTo: "0xabc",
price: "$1.00" as Price,
network: "eip155:8453" as Network,
},
},
};

const httpServer = new x402HTTPResourceServer(ResourceServer, routes);

const adapter = new MockHTTPAdapter();
const context: HTTPRequestContext = {
adapter,
path: "/api/users/42",
method: "GET",
};

const result = await httpServer.processHTTPRequest(context);

expect(result.type).toBe("payment-error"); // Route matched
});

it("should not match :param against paths with extra segments", async () => {
const routes = {
"/api/users/:id": {
accepts: {
scheme: "exact",
payTo: "0xabc",
price: "$1.00" as Price,
network: "eip155:8453" as Network,
},
},
};

const httpServer = new x402HTTPResourceServer(ResourceServer, routes);

const adapter = new MockHTTPAdapter();
const context: HTTPRequestContext = {
adapter,
path: "/api/users/42/posts",
method: "GET",
};

const result = await httpServer.processHTTPRequest(context);

expect(result.type).toBe("no-payment-required");
});

it("should return no-payment-required for unmatched routes", async () => {
const routes = {
"/api/protected": {
Expand Down
63 changes: 63 additions & 0 deletions typescript/packages/mechanisms/tvm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# @x402/tvm

TVM (TON) mechanism for the [x402 payment protocol](https://github.com/coinbase/x402).

Supports gasless USDT payments on TON via self-relay gas sponsorship using W5R1 wallets. The client makes **zero blockchain calls** — all on-chain interaction is handled by the facilitator service.

## Installation

```bash
npm install @x402/tvm @x402/core
```

## Quick Start

### Client (Buyer)

```typescript
import { createTvmClient, toClientTvmSigner } from "@x402/tvm/exact/client";
import { mnemonicToPrivateKey } from "@ton/crypto";

const keyPair = await mnemonicToPrivateKey(mnemonic.split(" "));
const signer = toClientTvmSigner(keyPair);
const client = createTvmClient({ signer });
```

### Server (Seller)

```typescript
import { registerExactTvmScheme } from "@x402/tvm/exact/server";

registerExactTvmScheme(server, { networks: ["tvm:-239"] });
```

### Facilitator

```typescript
import { registerExactTvmScheme } from "@x402/tvm/exact/facilitator";

registerExactTvmScheme(facilitator, {
facilitatorUrl: "https://ton-facilitator.okhlopkov.com",
networks: ["tvm:-239"],
});
```

## Architecture

The TON mechanism uses **self-relay**: the facilitator sponsors gas so clients never need TON.

1. Client calls facilitator `/prepare` → gets seqno + messages to sign
2. Client signs W5R1 `internal_signed` transfer (zero blockchain calls)
3. Merchant calls facilitator `/verify` + `/settle`
4. Facilitator relays the signed transfer on-chain, sponsoring gas

## Networks

| Network | CAIP-2 ID | Description |
|---------|-----------|-------------|
| TON Mainnet | `tvm:-239` | Production network |
| TON Testnet | `tvm:-3` | Test network |

## License

MIT
Loading