Skip to content
Merged
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
31 changes: 21 additions & 10 deletions examples/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ A full client → server → facilitator loop:
| [`facilitator/basic`](facilitator/basic) | verifies + settles on-chain | 4022 | agent-wallet (EVM + TRON) |
| [`servers/express`](servers/express) | sells `GET /weather` behind 402 | 4021 | none (keyless, payout address only) |
| [`clients/fetch`](clients/fetch) | pays automatically via wrapped `fetch` | — | agent-wallet (EVM + TRON) |
| [`servers/mcp`](servers/mcp) | sells the `get_weather` MCP tool behind 402 | 4023 | none (keyless, payout address only) |
| [`clients/mcp`](clients/mcp) | pays automatically over MCP/SSE | — | agent-wallet (EVM + TRON) |

The `mcp` pair is the same line over an **MCP transport** — it shares
`facilitator/basic` and `.env-exact`, swapping the HTTP server/client for an MCP
server (`pnpm dev:mcp-server`, :4023) and client (`pnpm dev:mcp-client`); `ping`
is free, `get_weather` is paid.

## GasFree scenario (TRON `exact_gasfree`)

Expand Down Expand Up @@ -95,6 +102,7 @@ amount and strips the header. BSC uses USDC, TRON Nile uses USDT — both Permit
| TRON nile | `tron:0xcd8690dc` | USDD (18) | exact **permit2** | yes — client auto-broadcasts | only the approve (TRX) |
| BSC testnet | `eip155:97` | DHLU (6) | exact **eip3009** | none | **none (fully gasless)** |
| BSC testnet | `eip155:97` | USDC (18) | exact **permit2 + gas-sponsoring** | yes — client signs, facilitator relays | only the approve (BNB) |
| BSC testnet | `eip155:97` | USDT (18) | exact **permit2 + gas-sponsoring** | yes — client signs, facilitator relays | only the approve (BNB) |

How each token settles on-chain:

Expand All @@ -106,8 +114,8 @@ How each token settles on-chain:
`approve(Permit2)` (`ensureAllowance`, ~3s); the facilitator settles via
`x402Permit2Proxy.settle`. TRON has no gas delegation, so that approve costs
the user a little TRX.
- **BSC USDC — permit2 + gas-sponsoring:** USDC is a plain BEP-20 (no
ERC-3009 / no EIP-2612), so it needs an on-chain `approve(Permit2)`. The
- **BSC USDC/USDT — permit2 + gas-sponsoring:** both are plain BEP-20 (no
ERC-3009 / no EIP-2612), so each needs an on-chain `approve(Permit2)`. The
client signs that approve **offline**; the facilitator broadcasts it bundled
with settle (the `erc20ApprovalGasSponsoring` extension). This removes the
separate manual approve step — but the approve's `from` is the user, so on a
Expand All @@ -119,22 +127,25 @@ authorizer/fee-payer split, so the **client** broadcasts it; EVM can decouple
them, so the **facilitator** relays it (leaving the door open to real
sponsorship).

### Mainnet (commented-ready, real funds)
### Mainnet (real funds)

Production tokens are pre-listed as **commented** entries in the same tables, so
enabling them is config-only — no logic changes (see "Adding a chain/token"
below). All verified on-chain; sources: the official
Mainnet networks (`eip155:56`, `tron:0x2b6653dc`) are registered alongside their
testnet counterparts in the same tables — no uncommenting needed. The server
advertises both networks, and the client selects which one to pay through
`PAY_TARGETS`. The example uses one `EVM_ADDRESS` / `TRON_ADDRESS` payout address
for both the testnet and mainnet entries; configure addresses that are valid for
that shared setup, and set a reliable `EVM_RPC_URL` for BSC. ⚠️ **REAL FUNDS.**
All verified on-chain; sources: the official
[Network & Token Support](https://docs.bankofai.io/x402/core-concepts/network-and-token-support/) docs.

| Network | Token | Address | Dec | Method |
|---|---|---|---|---|
| `eip155:56` (BSC) | USDC | `0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d` | 18 | permit2 |
| `eip155:56` (BSC) | USDT | `0x55d398326f99059fF775485246999027B3197955` | 18 | permit2 |
| `eip155:56` (BSC) | EPS | `0xA7f552078dcC247C2684336020c03648500C6d9F` | 18 | permit2 |
| `tron:0x2b6653dc` | USDT | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` | 6 | permit2 |
| `tron:0x2b6653dc` | USDD | `TXDk8mbtRbXeYuMNS83CfKPaYYT8XWv9Hz` | 18 | permit2 |

> BSC mainnet has **no ERC-3009 token** (USDC/USDT/EPS are all plain BEP-20) — so
> BSC mainnet has **no ERC-3009 token** (USDC/USDT are plain BEP-20) — so
> mainnet EVM payments all go permit2 + gas-sponsored approve. DHLU (eip3009) is
> BSC-testnet-only.

Expand Down Expand Up @@ -169,11 +180,11 @@ chain reads and broadcast.
## Run

Env is **split by business scenario** — one self-contained file per line, so the
three lines run side by side and you only fill in what you run:
four lines run side by side and you only fill in what you run:

| Scenario | Template → fill | Loaded by |
|---|---|---|
| exact (main) | `.env-exact.example` → `.env-exact` | `clients/fetch`, `servers/express`, `facilitator/basic` |
| exact (main) | `.env-exact.example` → `.env-exact` | `clients/fetch`, `servers/express`, `facilitator/basic`, `clients/mcp`, `servers/mcp` |
| gasfree | `.env-gasfree.example` → `.env-gasfree` | `clients/gasfree`, `servers/gasfree`, `facilitator/gasfree` |
| batch-settlement | `.env-batch.example` → `.env-batch` | `clients/batch-settlement`, `servers/batch-settlement`, `facilitator/batch-settlement` |
| upto | `.env-upto.example` → `.env-upto` | `clients/upto`, `servers/upto`, `facilitator/upto` |
Expand Down
11 changes: 6 additions & 5 deletions examples/typescript/clients/fetch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ Wraps `fetch` so HTTP `402 Payment Required` challenges are paid automatically.

Both chains sign via `@bankofai/agent-wallet` (`resolveWallet({ network })`):

- EVM: `createClientEvmSigner(wallet, publicClient)` — adapts the wallet
(async address + `0x`-normalized signatures) and wires `readContract` for
permit2 enrichment.
- TRON: `createClientTronSigner(tronWeb, agentWallet)` — the SDK takes its
`AgentWallet` shape, so `chains/tron.ts` adapts the raw wallet inline.
- EVM: `createClientEvmSigner(wallet, { network, rpcUrl })` — adapts the wallet
(async address + `0x`-normalized signatures); the factory builds the viem
`publicClient` internally and wires `readContract` for permit2 enrichment.
- TRON: `createClientTronSigner(wallet, { network, apiKey })` — the factory
builds TronWeb internally from the network and auto-broadcasts the one-time
Permit2 approve for USDT/USDD.

A chain registers only if its wallet resolves, so you can pay from EVM-only,
TRON-only, or both.
Expand Down
8 changes: 4 additions & 4 deletions examples/typescript/facilitator/basic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ HTTP layer (`src/index.ts`) is **chain-agnostic** and dispatches by the payment'

The signer factories are **wallet-only**:

- EVM: `createFacilitatorEvmSigner(publicClient, wallet)`
- TRON: `createFacilitatorTronSigner(tronWeb, wallet)`
- EVM: `createFacilitatorEvmSigner(wallet, { network, rpcUrl })`
- TRON: `createFacilitatorTronSigner(wallet, { network, apiKey })`

The `wallet` comes from `resolveWallet({ network })` in `@bankofai/agent-wallet`,
which loads the secret out-of-band (env / keystore / Privy). **This example never
reads a private key.** The viem `publicClient` / `TronWeb` carry no account — they
only do chain reads and broadcast.
reads a private key.** The factories build the viem `publicClient` / `TronWeb`
internally and they carry no account — they only do chain reads and broadcast.

A chain registers only if its wallet resolves, so you can run EVM-only,
TRON-only, or both.
Expand Down
11 changes: 11 additions & 0 deletions typescript/.changeset/tron-remove-advisory-fee.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@bankofai/x402-tron": major
---

**Breaking:** Remove advisory fee configuration and metadata from the TRON
`exact` and `upto` schemes. These schemes now transfer exactly the requested
amount and no longer accept a fee configuration through
`TronFacilitatorConfig` or their facilitator constructors.

GasFree fee handling is unchanged because those fees are enforced by the
GasFree relayer rather than advertised as advisory payment metadata.
60 changes: 44 additions & 16 deletions typescript/packages/mechanisms/tron/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ This package provides the three x402 mechanism roles for TRON, mirroring `@banko

It plugs into `@bankofai/x402-core` via the `tron:*` CAIP family — no core changes are required.

### Fee behavior

The `exact` and `upto` schemes do not advertise or collect a facilitator fee:
their deployed proxies transfer exactly the payment amount. Facilitator fees are
supported only by `exact_gasfree`, where the relayer deducts the configured fee
from the GasFree wallet.

When upgrading from the advisory-fee API, remove the `fee` property from
`TronFacilitatorConfig` and stop passing a fee configuration as the second
argument to the `exact` or `upto` facilitator scheme constructors. Pass the fee
configuration only when registering or constructing an `exact_gasfree`
facilitator.

## Two transfer paths (and why TRON is effectively Permit2)

The exact scheme supports two `assetTransferMethod`s, selected via `extra.assetTransferMethod`:
Expand All @@ -37,7 +50,7 @@ The `permit2` path binds the destination with a Permit2 **witness**. For the exa
Witness(address to, uint256 validAfter)
```

The client TIP-712 type, the proxy `settle` ABI, and the on-chain `WITNESS_TYPEHASH` must stay in sync — `test/unit/permit2-digest.test.ts` enforces this. (The `upto` proxy uses a 3-field witness with a `facilitator` field; this package implements `exact` only.)
The client TIP-712 type, the proxy `settle` ABI, and the on-chain `WITNESS_TYPEHASH` must stay in sync — `test/unit/permit2-digest.test.ts` enforces this. (The `upto` proxy uses a 3-field witness with a `facilitator` field; this section describes the `exact` witness only.)

## Package exports

Expand All @@ -47,6 +60,13 @@ The client TIP-712 type, the proxy `settle` ABI, and the on-chain `WITNESS_TYPEH
| `@bankofai/x402-tron/exact/client` | client scheme + register + Permit2 helpers |
| `@bankofai/x402-tron/exact/server` | server scheme + register |
| `@bankofai/x402-tron/exact/facilitator` | facilitator scheme + register |
| `@bankofai/x402-tron/upto/client` | upto client scheme + register + Permit2 helpers |
| `@bankofai/x402-tron/upto/server` | upto server scheme + register |
| `@bankofai/x402-tron/upto/facilitator` | upto facilitator scheme + register |
| `@bankofai/x402-tron/gasfree/client` | exact_gasfree client scheme + register |
| `@bankofai/x402-tron/gasfree/server` | exact_gasfree server scheme + register |
| `@bankofai/x402-tron/gasfree/facilitator` | exact_gasfree facilitator scheme + register |
| `@bankofai/x402-tron/batch-settlement/*` | batch-settlement client/server/facilitator |

## Usage

Expand All @@ -57,17 +77,17 @@ import { TronWeb } from "tronweb";
import {
createClientTronSigner,
createFacilitatorTronSigner,
type AgentWallet,
type FacilitatorAgentWallet,
type ClientTronWallet,
type FacilitatorTronWallet,
} from "@bankofai/x402-tron";

const privateKey = process.env.TRON_PRIVATE_KEY!.replace(/^0x/, "");
const tronWeb = new TronWeb({ fullHost: "https://nile.trongrid.io" });
const address = TronWeb.address.fromPrivateKey(privateKey) as string;

// Client: signs TIP-712 typed data. The private key stays in your wallet —
// the SDK only sees the AgentWallet interface, never the raw key.
const clientWallet: AgentWallet = {
// the SDK only sees the ClientTronWallet interface, never the raw key.
const clientWallet: ClientTronWallet = {
getAddress: () => address,
async signTypedData(args) {
const sig = await tronWeb.trx._signTypedData(
Expand All @@ -79,22 +99,26 @@ const clientWallet: AgentWallet = {
return (sig.startsWith("0x") ? sig : `0x${sig}`) as `0x${string}`;
},
};
const clientSigner = await createClientTronSigner(tronWeb, clientWallet);
const clientSigner = await createClientTronSigner(clientWallet, {
network: "tron:0xcd8690dc",
});

// Facilitator: signs built settlement transactions for on-chain broadcast.
const facilitatorWallet: FacilitatorAgentWallet = {
address,
const facilitatorWallet: FacilitatorTronWallet = {
getAddress: () => address,
async signTransaction(transaction) {
return tronWeb.trx.sign(transaction, privateKey);
},
};
const facilitatorSigner = createFacilitatorTronSigner(tronWeb, facilitatorWallet);
const facilitatorSigner = await createFacilitatorTronSigner(facilitatorWallet, {
network: "tron:0xcd8690dc",
});
```

`createClientTronSigner` is async (it resolves the wallet address);
`createFacilitatorTronSigner` is synchronous. Both also expose
`toClientTronSigner` / `toFacilitatorTronSigner` adapters if you already have a
signing object that matches the full signer shape.
Both `createClientTronSigner` and `createFacilitatorTronSigner` are `async`
(they resolve the wallet address and build the TronWeb client from `network`).
Pass the wallet as the first argument and `{ network, rpcUrl?, apiKey? }` as
options — the factories build the TronWeb instance internally.

### Client

Expand Down Expand Up @@ -131,10 +155,14 @@ The `permit2` path depends on three on-chain contracts. Addresses live in `src/c
| Network | chainId (TIP-712) | Permit2 | x402ExactPermit2Proxy |
| --- | --- | --- | --- |
| `tron:0xcd8690dc` | 3448148188 | `TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h` | `TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F` |
| `tron:0x2b6653dc` | 728126428 | *(placeholder — replace)* | *(placeholder — replace)* |
| `tron:0x2b6653dc` | 728126428 | `TTJxU3P8rHycAyFY4kVtGNfmnMH4ezcuM9` | `TN49yaJmZMZoEdDCqjB4uPzQLHvYkGw95m` |
| `tron:0x94a9059e` | 2494104990 | — (no Permit2 deployment) | — |

> ⚠️ **Before mainnet:** deploy your own audited Permit2 + `x402ExactPermit2Proxy` and replace the addresses in `constants.ts`. Do not reuse placeholder/testnet addresses. The proxy `PERMIT2()` immutable must equal the configured Permit2 address, and its `WITNESS_TYPEHASH` must match the 2-field witness.
> ⚠️ Mainnet uses real funds. The configured Permit2 and
> `x402ExactPermit2Proxy` addresses are the deployed mainnet contracts; verify
> them against the current release configuration before production use. The
> proxy `PERMIT2()` immutable must equal the configured Permit2 address, and its
> `WITNESS_TYPEHASH` must match the 2-field witness.

## Testing

Expand All @@ -148,7 +176,7 @@ The suite is fully offline (no network, no keys):

- `permit2-digest.test.ts` — reproduces the exact on-chain Permit2 digest and recovers the signer two independent ways (the facilitator verify path and a manual contract-style reconstruction), guaranteeing TIP-712 hashing matches the deployed proxy.
- `gasfree-digest.test.ts` / `gasfree-flow.test.ts` — GasFree TIP-712 sign↔verify round-trip plus client/facilitator term-validation and settle flow against a mocked relayer.
- `tokens.test.ts`, `fee.test.ts`, `fee-plumbing.test.ts`, `selection.test.ts`, `signer-wallet.test.ts` — token registry, fee policy, fee advertisement plumbing, token selection / balance filtering, and the AgentWallet abstraction.
- `tokens.test.ts`, `fee.test.ts`, `selection.test.ts`, `signer-wallet.test.ts` — token registry, fee policy, token selection / balance filtering, and the ClientTronWallet abstraction.

## Notes & caveats

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
} from "../../constants";
import { ClientTronSigner } from "../../signer";
import { ExactPermit2Payload } from "../../types";
import { readFeeFromExtra } from "../../shared/fee";
import { createNonce, getTronChainId, normalizeAddressForSigning } from "../../utils";

/**
Expand Down Expand Up @@ -63,13 +62,11 @@ export async function createPermit2Payload(

// Ensure the one-time Permit2 allowance before signing (mirrors the Python
// client). No-op when the signer can't broadcast (sign-only wallet) or when
// the allowance already covers payment + fee. TRON's mainstream tokens
// the allowance already covers the payment. TRON's mainstream tokens
// (USDT/USDD) lack ERC-3009, so this approve is required on first use.
const feeAmount = readFeeFromExtra(paymentRequirements.extra)?.feeAmount ?? "0";
const totalRequired = BigInt(paymentRequirements.amount) + BigInt(feeAmount);
await signer.ensureAllowance?.({
token: paymentRequirements.asset,
amount: totalRequired,
amount: BigInt(paymentRequirements.amount),
network,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { x402Facilitator } from "@bankofai/x402-core/facilitator";
import { Network } from "@bankofai/x402-core/types";
import { FacilitatorTronSigner } from "../../signer";
import { ExactTronFeeConfig } from "../../shared/fee";
import { ExactTronScheme } from "./scheme";

/**
Expand All @@ -10,8 +9,6 @@ import { ExactTronScheme } from "./scheme";
export interface TronFacilitatorConfig {
signer: FacilitatorTronSigner;
networks: Network | Network[];
/** Optional facilitator fee configuration (advertised via getExtra). */
fee?: ExactTronFeeConfig;
}

/**
Expand All @@ -25,6 +22,6 @@ export function registerExactTronScheme(
facilitator: x402Facilitator,
config: TronFacilitatorConfig,
): x402Facilitator {
facilitator.register(config.networks, new ExactTronScheme(config.signer, config.fee));
facilitator.register(config.networks, new ExactTronScheme(config.signer));
return facilitator;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import { FacilitatorTronSigner } from "../../signer";
import { ExactEIP3009Payload, ExactTronPayload, isPermit2Payload } from "../../types";
import { X402_PERMIT2_PROXY_ADDRESSES } from "../../constants";
import { ExactTronFeeConfig } from "../../shared/fee";
import { verifyEIP3009, settleEIP3009 } from "./eip3009";
import { verifyPermit2, settlePermit2 } from "./permit2";

Expand All @@ -25,15 +24,8 @@ export class ExactTronScheme implements SchemeNetworkFacilitator {
* Creates a new ExactTronScheme facilitator instance.
*
* @param signer - The TRON signer for facilitator operations
* @param feeConfig - Optional facilitator fee configuration. On the deployed
* exact/permit2 proxy the fee is advisory only (the proxy transfers exactly
* `amount` and does not split a fee); it is advertised so clients and other
* schemes (e.g. GasFree) can observe and enforce it.
*/
constructor(
private readonly signer: FacilitatorTronSigner,
private readonly feeConfig: ExactTronFeeConfig = {},
) {}
constructor(private readonly signer: FacilitatorTronSigner) {}

/**
* Gets extra configuration for the facilitator.
Expand All @@ -55,21 +47,6 @@ export class ExactTronScheme implements SchemeNetworkFacilitator {
...(signers.length > 0 && X402_PERMIT2_PROXY_ADDRESSES[network]
? { permit2FacilitatorAddress: signers[0] }
: {}),
// Advertise the fee configuration so the server can attach per-asset fee
// terms to requirements (extra.fee) and clients/other schemes can observe
// it. Only emitted when a baseFee is configured.
...(this.feeConfig.baseFee
? {
feeConfig: {
feeTo: this.feeConfig.feeTo ?? signers[0],
...(this.feeConfig.caller ? { caller: this.feeConfig.caller } : {}),
baseFee: this.feeConfig.baseFee,
...(this.feeConfig.allowedTokens
? { allowedTokens: this.feeConfig.allowedTokens }
: {}),
},
}
: {}),
};
}

Expand Down
Loading
Loading