Skip to content

feat: fallback RPC for TRON mainnet without API key#60

Merged
Hades-Ye merged 3 commits intomainfrom
feature/fallback-rpc-no-api-key
Mar 30, 2026
Merged

feat: fallback RPC for TRON mainnet without API key#60
Hades-Ye merged 3 commits intomainfrom
feature/fallback-rpc-no-api-key

Conversation

@Hades-Ye
Copy link
Copy Markdown
Contributor

Summary

  • When TRON_GRID_API_KEY is not set, the TRON signer now uses https://hptg.bankofai.io as the mainnet RPC instead of the rate-limited TronGrid endpoint
  • When TRON_GRID_API_KEY is set, behavior is unchanged (uses TronGrid with the API key)
  • Applies to both TypeScript and Python SDKs

🤖 Generated with Claude Code

… not set

When TRON_GRID_API_KEY is not configured, the TRON signer now uses
https://hptg.bankofai.io as the mainnet RPC instead of the rate-limited
TronGrid endpoint. Existing behavior is preserved when the API key is set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Code Review Report

Project: x402 (BankOfAI x402 Payment Protocol SDK)
PR: mainfeature/fallback-rpc-no-api-key
Review Date: 2026-03-30
Reviewer: AI Code Reviewer (Code Review Skill v1.0.0)


PR Overview

Branch Information

Property Value
From Branch main
To Branch feature/fallback-rpc-no-api-key
Commits 1
Files Changed 6
Lines Added +216
Lines Removed -4

Commit History

Hash Message
52d3f7f feat: use fallback RPC URL for TRON mainnet when TRON_GRID_API_KEY is not set

Review Summary

Verdict

Verdict: Request Changes

Findings at a Glance

Critical Major Minor Suggestion
Count 1 4 3 3

Summary

This PR introduces a fallback RPC endpoint (https://hptg.bankofai.io) for TRON mainnet across both Python and TypeScript SDKs, triggered silently whenever TRON_GRID_API_KEY is absent. The intent — improving developer onboarding by avoiding rate-limit failures out of the box — is sound and useful. The implementation is clean and reasonably well tested.

However, there is one critical security concern that must be addressed before merging: this SDK handles real financial transactions on TRON mainnet (USDT payments, balance checks, allowance approvals), and all such traffic is silently routed to a single, hardcoded, first-party-controlled endpoint without any explicit user consent, opt-out mechanism, or prominent warning. For a payments SDK, this creates a significant trust and privacy risk — the endpoint operator has full visibility into wallet addresses, balances, and payment patterns for every user without an API key.

Additionally, four major issues are present: the TypeScript fallback is completely silent (no log entry emitted), integration tests make live network calls to the proprietary endpoint (making the CI suite non-deterministic and externally dependent), the resolveRpcUrl function is not updated to use the new fallback logic (creating inconsistency), and the TypeScript integration test uses a token contract address as the wallet address, which is an unrealistic test scenario.


Change Summary

1. Python fallback — tron_client.py

File Change Type Description
python/x402/src/bankofai/x402/utils/tron_client.py Modified Adds an early-return branch in create_async_tron_client: when TRON_GRID_API_KEY is unset and network is mainnet, instantiates AsyncTron with a hardcoded fallback AsyncHTTPProvider pointing at https://hptg.bankofai.io.

Purpose: Prevent mainnet calls from failing out of the box due to TronGrid rate limits when no API key is configured.


2. Python tests — test_tron_client_factory.py

File Change Type Description
python/x402/tests/utils/test_tron_client_factory.py Added New test file with 4 unit tests (mocked) + 2 functional tests, covering no-key/mainnet, tron-prefix stripping, nile passthrough, and API-key-set behaviour. Includes a live-network anyio async test.

Purpose: Validate the new fallback URL selection logic in Python.


3. TypeScript config — config.ts + config.test.ts

File Change Type Description
typescript/packages/x402/src/config.ts Modified Adds TRON_FALLBACK_RPC_URLS constant and the new getTronRpcUrl(network) exported function.
typescript/packages/x402/src/config.test.ts Added 6 unit tests for getTronRpcUrl, covering all permutations of key-set/unset × known/unknown networks.

Purpose: Centralise TRON RPC URL resolution with fallback logic in TypeScript.


4. TypeScript signer — signer.ts + tronSigner.test.ts

File Change Type Description
typescript/packages/x402/src/signers/signer.ts Modified getTronWeb() now calls getTronRpcUrl(network) instead of indexing TRON_RPC_URLS[network] directly.
typescript/packages/x402/src/signers/tronSigner.test.ts Added One integration test that spies on getTronRpcUrl, constructs a TronClientSigner, and calls checkBalance against the live fallback endpoint.

Purpose: Wire the signer to the new fallback-aware RPC URL resolver.


Detailed Findings


Critical

[C-01] Silent re-routing of all mainnet financial traffic to a proprietary, hardcoded endpoint without user consent

Property Value
Severity Critical
Category Security / Trust
File python/x402/src/bankofai/x402/utils/tron_client.py : Lines 35–42 · typescript/packages/x402/src/config.ts : Lines 79–93

Description

This SDK processes real TRON mainnet financial transactions — balance checks, TRC-20 allowance queries, approve transactions, and payment permit signatures. When TRON_GRID_API_KEY is absent (the common developer default), 100% of mainnet RPC traffic is silently redirected to https://hptg.bankofai.io, a first-party-controlled endpoint, without:

  • any explicit warning at WARNING or higher log level (Python logs INFO; TypeScript logs nothing at all),
  • any user opt-in, opt-out, or configuration mechanism,
  • any documentation visible at call-site that data is being sent elsewhere.

The practical consequences for a payments SDK are severe:

  1. Privacy: the endpoint operator receives wallet addresses, token contract queries, balance values, and timing patterns for every user who hasn't set a TRON_GRID_API_KEY. This data is commercially sensitive.
  2. Integrity: a compromised or malicious endpoint could return crafted RPC responses — falsified balances, altered contract ABIs, fabricated triggerConstantContract results — that mislead client-side payment logic without triggering any visible error.
  3. Availability as a single point of failure: there is no multi-endpoint fallback, health-check, or retry logic. If hptg.bankofai.io is unreachable, all no-key mainnet users are broken in exactly the same way as before.
  4. Conflict of interest: the SDK vendor routing unauthenticated traffic to their own infrastructure is an architectural pattern that warrants explicit, prominent disclosure at the SDK documentation and API level, not just a silent code path.

Code

# tron_client.py  Lines 35-42
if network == "mainnet":
    fallback_url = "https://hptg.bankofai.io"
    logger.info(                          # <-- INFO, not WARNING
        "TRON_GRID_API_KEY is not set. Using fallback RPC for mainnet: %s",
        fallback_url,
    )
    provider = AsyncHTTPProvider(endpoint_uri=fallback_url)
    return AsyncTron(provider=provider, network=network)
// config.ts  Lines 79-93
export const TRON_FALLBACK_RPC_URLS: Record<string, string> = {
  'tron:mainnet': 'https://hptg.bankofai.io',  // always used; no opt-out
};

export function getTronRpcUrl(network: string): string | undefined {
  const apiKey = typeof process !== 'undefined' ? process.env?.TRON_GRID_API_KEY : undefined;
  if (!apiKey) {
    return TRON_FALLBACK_RPC_URLS[network] ?? TRON_RPC_URLS[network];
    // No log entry at all in TypeScript
  }
  return TRON_RPC_URLS[network];
}

Recommendation

Before merging, address at minimum the following:

  1. Escalate the log level to WARNING (Python) and emit an equivalent console.warn (TypeScript) that explicitly names the endpoint and notes that all RPC calls will be routed there.
  2. Respect a user-configured override: check TRON_RPC_URL or TRON_FALLBACK_RPC_URL env var first; only use the hardcoded URL if that is also absent.
  3. Document the behaviour: add a note in the SDK README and in the docstring of create_async_tron_client / getTronRpcUrl that the fallback endpoint is operated by BankOfAI, is subject to rate limits and availability, and how to override it.
  4. Consider opt-in: make the fallback opt-in rather than opt-out, at least until the privacy/disclosure concern is addressed upstream.
# Recommended minimum change (Python)
fallback_url = os.getenv("TRON_RPC_URL", "https://hptg.bankofai.io")
logger.warning(
    "TRON_GRID_API_KEY is not set. Mainnet RPC calls will be routed to %s "
    "(BankOfAI-operated fallback). Set TRON_GRID_API_KEY or TRON_RPC_URL to override.",
    fallback_url,
)

Major

[MJ-01] TypeScript fallback activation is completely silent — no log entry emitted

Property Value
Severity Major
Category Observability / Security
File typescript/packages/x402/src/config.ts : Lines 87–93

Description

The Python path at least logs at INFO level that the fallback URL is being used. The TypeScript getTronRpcUrl implementation silently returns the fallback URL with zero observability output. Any TypeScript/Node.js application using this SDK will have its mainnet TRON calls routed through https://hptg.bankofai.io with no indication in any log stream. This is especially problematic in server-side payment facilitator code where operators rely on logs to diagnose connectivity issues.

Code

// config.ts  Lines 87-93
export function getTronRpcUrl(network: string): string | undefined {
  const apiKey = typeof process !== 'undefined' ? process.env?.TRON_GRID_API_KEY : undefined;
  if (!apiKey) {
    // No console.warn, no logger call — complete silence
    return TRON_FALLBACK_RPC_URLS[network] ?? TRON_RPC_URLS[network];
  }
  return TRON_RPC_URLS[network];
}

Recommendation

export function getTronRpcUrl(network: string): string | undefined {
  const apiKey = typeof process !== 'undefined' ? process.env?.TRON_GRID_API_KEY : undefined;
  if (!apiKey) {
    const fallback = TRON_FALLBACK_RPC_URLS[network];
    if (fallback) {
      console.warn(
        `[x402] TRON_GRID_API_KEY is not set. Routing ${network} RPC calls ` +
        `to fallback endpoint: ${fallback}. Set TRON_GRID_API_KEY to use TronGrid.`
      );
      return fallback;
    }
    return TRON_RPC_URLS[network];
  }
  return TRON_RPC_URLS[network];
}

[MJ-02] Integration tests make live network calls to the proprietary fallback endpoint

Property Value
Severity Major
Category Testing
File python/x402/tests/utils/test_tron_client_factory.py : Lines 80–89 · typescript/packages/x402/src/signers/tronSigner.test.ts : Lines 35–53

Description

Two tests make real outbound HTTP calls to https://hptg.bankofai.io rather than mocking the network layer:

  • TestFallbackClientFunctional.test_fallback_client_can_fetch_block (Python) — fetches a real block.
  • TronClientSigner fallback RPCcheckBalance test (TypeScript) — calls triggerConstantContract on mainnet.

Consequences:

  • CI becomes non-deterministic: the test suite will fail if the fallback endpoint is down, rate-limiting, or slow.
  • Tests are environment-dependent: they require outbound internet from the CI runner.
  • They create an implicit monitoring dependency on hptg.bankofai.io — if the endpoint degrades the test suite breaks before any application-level alert would fire.
  • The TypeScript test asserts balance > BigInt(0), which will silently pass or fail depending on live on-chain state.

Code

# test_tron_client_factory.py  Lines 80-89
@pytest.mark.anyio
@patch.dict("os.environ", {}, clear=True)
async def test_fallback_client_can_fetch_block(self):
    """The fallback client should be able to fetch the latest block."""
    client = create_async_tron_client("tron:mainnet")
    async with client:
        block = await client.get_latest_block()   # <-- real network call
        assert block["block_header"]["raw_data"]["number"] > 0
// tronSigner.test.ts  Lines 42-50
const balance = await signer.checkBalance(MAINNET_USDT, 'tron:mainnet');
// ...
expect(balance).toBeGreaterThan(BigInt(0));  // <-- real RPC call, live assertion

Recommendation

Move live-network tests to a dedicated integration test suite guarded by an environment variable (e.g., TEST_INTEGRATION=true) and excluded from the default CI run. For the unit test suite, mock the HTTP provider's response.

# Mark as integration test and skip by default
@pytest.mark.integration
@pytest.mark.anyio
async def test_fallback_client_can_fetch_block(self): ...
// vitest.config.ts — exclude integration tests by default
// Run with: TEST_INTEGRATION=true vitest run --reporter=verbose

[MJ-03] resolveRpcUrl not updated — partial implementation creates inconsistent URL resolution

Property Value
Severity Major
Category Correctness
File typescript/packages/x402/src/config.ts : Lines 99–101

Description

The PR introduces getTronRpcUrl as the correct, fallback-aware resolver for TRON networks, but the pre-existing resolveRpcUrl function is left unchanged. It continues to return TRON_RPC_URLS[network] directly, bypassing fallback logic entirely:

export function resolveRpcUrl(network: string): string | undefined {
  return EVM_RPC_URLS[network] ?? TRON_RPC_URLS[network];  // ignores fallback
}

Any code path that calls resolveRpcUrl('tron:mainnet') will receive the TronGrid URL (https://api.trongrid.io) regardless of whether an API key is set, which defeats the purpose of this PR for those callers and creates an inconsistency between two public functions that appear to serve similar roles.

Code

// config.ts  Lines 99-101 — NOT changed in this PR
export function resolveRpcUrl(network: string): string | undefined {
  return EVM_RPC_URLS[network] ?? TRON_RPC_URLS[network];
}

Recommendation

Update resolveRpcUrl to delegate to getTronRpcUrl for TRON networks:

export function resolveRpcUrl(network: string): string | undefined {
  if (network.startsWith('tron:')) {
    return getTronRpcUrl(network);
  }
  return EVM_RPC_URLS[network];
}

[MJ-04] TypeScript integration test uses a token contract address as the wallet/signer address — unrealistic scenario masks real behaviour

Property Value
Severity Major
Category Testing / Correctness
File typescript/packages/x402/src/signers/tronSigner.test.ts : Lines 11, 38–42

Description

The test sets both the wallet address and the token address to TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t (the USDT TRC-20 contract address on mainnet). This means checkBalance queries the USDT balance of the USDT contract itself, which happens to hold a large balance due to its treasury role. The test then asserts balance > 0, which passes trivially but does not validate any realistic user scenario.

More importantly, this means the test does not verify that checkBalance works correctly for a normal EOA wallet address, which is the actual use-case for which it is invoked in production.

Code

// tronSigner.test.ts  Lines 11-12, 38-42
const MAINNET_USDT = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';  // USDT contract address

const wallet = createMockWallet(MAINNET_USDT);   // wallet address = token address
const signer = new TronClientSigner(wallet);
signer.setAddress(MAINNET_USDT);                  // signer address = token address

const balance = await signer.checkBalance(MAINNET_USDT, 'tron:mainnet');
// Queries: "what is the USDT balance of the USDT contract itself?"
// Not: "what is the USDT balance of a real user wallet?"

Recommendation

Use a real (but dormant) EOA wallet address for the signer, and validate that the balance result is a valid bigint (possibly zero for an empty wallet). Or, mock the RPC response entirely so the test doesn't depend on on-chain state:

// Use a known empty wallet or mock the response
const TEST_WALLET = 'TFooBarAddress...'; // a real but empty EOA
// OR: intercept tw.transactionBuilder.triggerConstantContract with vi.fn()

Minor

[MN-01] Python create_async_tron_client docstring not updated to reflect fallback behaviour

Property Value
Severity Minor
Category Documentation
File python/x402/src/bankofai/x402/utils/tron_client.py : Lines 19–27

Description

The function docstring still describes the function as using "TronGrid API key from TRON_GRID_API_KEY" but does not mention the new fallback RPC path for mainnet. Developers relying on help(create_async_tron_client) or IDE tooltips will have an incomplete picture.

Recommendation

def create_async_tron_client(network: str) -> Any:
    """Create an AsyncTron client for the given network.

    For mainnet, uses the TronGrid API key from TRON_GRID_API_KEY env var if set.
    If TRON_GRID_API_KEY is not set and network is 'mainnet', a fallback public
    RPC endpoint (https://hptg.bankofai.io) is used automatically. Set
    TRON_GRID_API_KEY for production use to avoid rate limiting.

    For non-mainnet networks without an API key, tronpy defaults are used.
    ...
    """

[MN-02] Fallback URL is a magic string duplicated across Python and TypeScript with no shared source of truth

Property Value
Severity Minor
Category Code Quality / Maintainability
File python/x402/src/bankofai/x402/utils/tron_client.py : Line 36 · typescript/packages/x402/src/config.ts : Line 80

Description

"https://hptg.bankofai.io" appears as a string literal in four different places across the two language SDKs and their tests. If this endpoint URL changes, all four occurrences must be updated manually with no compiler or linter assistance to catch missed locations.

Code

fallback_url = "https://hptg.bankofai.io"   # tron_client.py:36
FALLBACK_URL = "https://hptg.bankofai.io"   # test_tron_client_factory.py:12
'tron:mainnet': 'https://hptg.bankofai.io', // config.ts:80
const FALLBACK_URL = 'https://hptg.bankofai.io'; // tronSigner.test.ts:12 & config.test.ts:17

Recommendation

The Python constant is already inlined — extract to a module-level constant and import it in the test. In TypeScript, TRON_FALLBACK_RPC_URLS is already exported from config.ts; the test files should import the constant rather than redeclaring it:

// tronSigner.test.ts and config.test.ts
import { TRON_FALLBACK_RPC_URLS } from '../config.js';
const FALLBACK_URL = TRON_FALLBACK_RPC_URLS['tron:mainnet'];

[MN-03] test_fallback_client_has_correct_rpc_url accesses client.provider.endpoint_uri — implementation-detail assertion fragile against tronpy version changes

Property Value
Severity Minor
Category Testing
File python/x402/tests/utils/test_tron_client_factory.py : Lines 74–78

Description

The test directly inspects client.provider.endpoint_uri, which is a private implementation detail of tronpy's AsyncHTTPProvider. If tronpy renames or restructures this attribute in a future version, the test will break with an AttributeError, giving a misleading failure mode.

Code

def test_fallback_client_has_correct_rpc_url(self):
    client = create_async_tron_client("tron:mainnet")
    assert isinstance(client.provider, AsyncHTTPProvider)
    assert client.provider.endpoint_uri == FALLBACK_URL  # internal attribute

Recommendation

The mocked unit tests (TestCreateAsyncTronClientFallback) already cover URL selection correctly without inspecting internals. Consider replacing this test with one that verifies the mock call arguments (already done in test_mainnet_uses_fallback_url_without_api_key), and removing the fragile attribute inspection.


Suggestions

[S-01] Make the fallback URL configurable via environment variable

File: python/x402/src/bankofai/x402/utils/tron_client.py, typescript/packages/x402/src/config.ts
Description: The fallback URL is fully hardcoded. Power users who run their own TRON full nodes or prefer a different public endpoint (e.g., https://trx.nownodes.io) have no way to substitute it without patching the SDK.
Suggestion: Honour a TRON_RPC_URL (or TRON_MAINNET_RPC_URL) environment variable before falling back to the hardcoded default. This costs one line of code and gives operators meaningful control.


[S-02] Asymmetric fallback for Python: non-mainnet networks without an API key still have no fallback

File: python/x402/src/bankofai/x402/utils/tron_client.py : Lines 44–49
Description: The nile and shasta test networks fall through to AsyncTron(network=network) with a generic warning when no API key is set. The treatment is asymmetric: mainnet gets a specific fallback, testnets get "may fail". Testnet users are likely developers — the group most likely to be running without an API key.
Suggestion: Consider providing fallback URLs for nile/shasta as well, or at minimum reference a concrete free testnet faucet endpoint in the warning message so developers know where to find one.


[S-03] Consider adding a CHANGELOG entry for this behaviour change

File: CHANGELOG.md
Description: The fallback routing is a silent behavioural change visible to end users (e.g., their network firewall logs will now see outbound connections to hptg.bankofai.io). Enterprise or regulated users may need to whitelist the endpoint. A CHANGELOG entry under a ### Changed or ### Added heading would ensure visibility.
Suggestion: Add a changelog entry noting the new fallback endpoint and how to override it.


Positive Observations

Area Observation
Test structure (Python) The TestCreateAsyncTronClientFallback class uses precise, isolated mocking (@patch.dict("os.environ", {}, clear=True)) that properly prevents env var leakage between tests.
Test coverage breadth (TypeScript) config.test.ts covers all six meaningful permutations of the getTronRpcUrl decision matrix (key set × key unset × mainnet × testnet × unknown).
tron: prefix stripping in tests The Python test test_mainnet_with_tron_prefix_uses_fallback explicitly validates the tron:mainnetmainnet normalisation path, catching a realistic input variation.
TypeScript getTronRpcUrl design The new function correctly handles browser environments via the typeof process !== 'undefined' guard, matching the pre-existing pattern in getGasFreeApiKey.
Signer refactor is minimal and surgical The signer.ts change is a one-line swap of TRON_RPC_URLS[network]getTronRpcUrl(network) with no unrelated changes, making the diff easy to review and reason about.
Python logging is present Unlike TypeScript, the Python implementation does at least emit an INFO log with the fallback URL, giving operators a findable audit trail if they scan logs.

Checklist Results

Category Items Checked Pass Fail N/A Notes
Correctness 6 5 1 0 resolveRpcUrl not updated (MJ-03)
Security 8 5 2 1 Proprietary endpoint without consent (C-01); no logging in TS (MJ-01); SSRF N/A
Performance 5 5 0 0 No performance regressions introduced
Code Quality 8 6 2 0 Magic string duplication (MN-02); missing docstring update (MN-01)
Testing 7 4 3 0 Live network calls in unit tests (MJ-02); unrealistic wallet address (MJ-04); fragile attribute access (MN-03)
Documentation 5 3 2 0 Missing docstring update (MN-01); no CHANGELOG entry (S-03)
Compatibility 4 4 0 0 No breaking changes; existing API-key flows unaffected
Observability 4 2 2 0 Python INFO-only log (C-01 sub-issue); TypeScript completely silent (MJ-01)

Disclaimer

This is an automated code review. It supplements but does not replace human review. The reviewer analyzed only the diff between main and feature/fallback-rpc-no-api-key. Runtime behaviour, integration testing, and deployment impact are not covered.


Report generated by Code Review Skill v1.0.0
Date: 2026-03-30

- Add console.warn in TypeScript when fallback URL is used
- Upgrade Python log level from INFO to WARNING for fallback
- Update resolveRpcUrl to delegate to getTronRpcUrl for TRON networks
- Extract fallback URL to module constant, import in tests instead of duplicating
- Use a real EOA wallet address in TypeScript signer test
- Update Python docstring to document fallback behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Code Review Report

Project: x402 (BankOfAI)
PR: mainfeature/fallback-rpc-no-api-key
Review Date: 2026-03-30
Reviewer: AI Code Reviewer (Code Review Skill v1.0.0)


PR Overview

Branch Information

Property Value
From Branch main
To Branch feature/fallback-rpc-no-api-key
Commits 2
Files Changed 6
Lines Added +262
Lines Removed -6

Commit History

Hash Message
94faeb0 fix: address code review feedback on fallback RPC
52d3f7f feat: use fallback RPC URL for TRON mainnet when TRON_GRID_API_KEY is not set

Review Summary

Verdict

Verdict: Request Changes

Findings at a Glance

Critical Major Minor Suggestion
Count 0 3 2 2

Summary

This PR introduces a fallback RPC endpoint (https://hptg.bankofai.io) for TRON mainnet in both Python and TypeScript when TRON_GRID_API_KEY is not set. The feature solves a real usability problem — without an API key, TronGrid rate-limits or rejects mainnet requests — by transparently routing unauthenticated traffic to a BankOfAI-controlled proxy. The implementation is logically sound and well-structured across both languages, with clear warning messages and consistent fallback logic.

However, there are three issues that should be resolved before merging. First, the TypeScript getTronRpcUrl() function emits console.warn on every call, not once at initialization; since this function is called for every checkBalance, checkAllowance, and ensureAllowance operation, production logs will be flooded. Second, two tests make live network calls to the fallback endpoint without any CI skip guards, making the test suite non-deterministic in offline or sandboxed environments. Third, the sole assertion in the TypeScript integration test (balance >= BigInt(0)) is vacuously true even when the network call silently fails and the catch block returns BigInt(0), providing false confidence.


Change Summary

1 · Python — Fallback RPC logic & tests

File Change Type Description
python/x402/src/bankofai/x402/utils/tron_client.py Modified Adds TRON_MAINNET_FALLBACK_URL constant; routes mainnet to fallback when API key absent
python/x402/tests/utils/test_tron_client_factory.py Added Unit and functional tests for the new fallback behaviour

Purpose: When TRON_GRID_API_KEY is absent and the network is mainnet, construct an AsyncHTTPProvider pointing at https://hptg.bankofai.io instead of falling through to tronpy's unauthenticated defaults.

2 · TypeScript — getTronRpcUrl helper & config

File Change Type Description
typescript/packages/x402/src/config.ts Modified Adds TRON_FALLBACK_RPC_URLS map and getTronRpcUrl() function; updates resolveRpcUrl() to delegate TRON networks to the new helper
typescript/packages/x402/src/config.test.ts Added Unit tests for getTronRpcUrl and resolveRpcUrl

Purpose: Mirror the Python fallback logic in TypeScript; expose getTronRpcUrl from index.ts so consumers can call it directly.

3 · TypeScript — Signer integration

File Change Type Description
typescript/packages/x402/src/signers/signer.ts Modified Replace TRON_RPC_URLS[network] lookup with getTronRpcUrl(network) in getTronWeb()
typescript/packages/x402/src/signers/tronSigner.test.ts Added Integration test verifying TronClientSigner.checkBalance uses the fallback URL on mainnet

Purpose: Wire TronClientSigner through the new helper so all TronWeb instance creation respects the fallback URL when the API key is absent.


Detailed Findings


Major

[MJ-01] console.warn fires on every RPC call, not once

Property Value
Severity Major
Category Code Quality / Performance
File typescript/packages/x402/src/config.ts : Lines 88–96

Description

getTronRpcUrl is called by TronClientSigner.getTronWeb() for every checkBalance, checkAllowance, and ensureAllowance invocation. Because the console.warn is placed unconditionally inside the hot path, each operation in a running agent will print the warning to stdout. In a busy payment-processing agent this will pollute logs, make real errors harder to spot, and surprise library consumers who just want one notification at startup.

Code

export function getTronRpcUrl(network: string): string | undefined {
  const apiKey = typeof process !== 'undefined' ? process.env?.TRON_GRID_API_KEY : undefined;
  if (!apiKey) {
    const fallback = TRON_FALLBACK_RPC_URLS[network];
    if (fallback) {
      console.warn(           // <-- fires every call
        `[x402] TRON_GRID_API_KEY is not set. Routing ${network} RPC calls ` +
        `to fallback endpoint: ${fallback}. Set TRON_GRID_API_KEY to use TronGrid.`
      );
      return fallback;
    }

Recommendation

Emit the warning once per process, using a module-level Set to track which networks have already been warned:

const _warnedNetworks = new Set<string>();

export function getTronRpcUrl(network: string): string | undefined {
  const apiKey = typeof process !== 'undefined' ? process.env?.TRON_GRID_API_KEY : undefined;
  if (!apiKey) {
    const fallback = TRON_FALLBACK_RPC_URLS[network];
    if (fallback) {
      if (!_warnedNetworks.has(network)) {
        _warnedNetworks.add(network);
        console.warn(
          `[x402] TRON_GRID_API_KEY is not set. Routing ${network} RPC calls ` +
          `to fallback endpoint: ${fallback}. Set TRON_GRID_API_KEY to use TronGrid.`
        );
      }
      return fallback;
    }
    ...
  }
}

Alternatively, move the single warning to the TronClientSigner constructor or getTronWeb at the point where the instance is first created.


[MJ-02] Live-network integration tests lack skip guards for CI

Property Value
Severity Major
Category Testing
File python/x402/tests/utils/test_tron_client_factory.py : Lines 81–90; typescript/packages/x402/src/signers/tronSigner.test.ts : Lines 38–56

Description

Two tests make real outbound network requests to https://hptg.bankofai.io:

  • Python TestFallbackClientFunctional::test_fallback_client_can_fetch_block calls client.get_latest_block() over the network.
  • TypeScript TronClientSigner fallback RPC calls signer.checkBalance(MAINNET_USDT, 'tron:mainnet'), which in turn issues a triggerConstantContract call to the fallback endpoint.

Neither test carries a marker or skipif guard that would allow CI pipelines without outbound internet access to skip them. Flaky network conditions or endpoint downtime will cause random CI failures entirely unrelated to the code under test.

Code

# Python — no skip marker, bare anyio
@pytest.mark.anyio
@patch.dict("os.environ", {}, clear=True)
async def test_fallback_client_can_fetch_block(self):
    client = create_async_tron_client("tron:mainnet")
    async with client:
        block = await client.get_latest_block()   # live network call
        assert block["block_header"]["raw_data"]["number"] > 0
// TypeScript — no .skipIf / describe.skipIf guard
it('uses fallback RPC URL and checkBalance succeeds on tron:mainnet without TRON_GRID_API_KEY', async () => {
  ...
  const balance = await signer.checkBalance(MAINNET_USDT, 'tron:mainnet');  // live call

Recommendation

Mark both tests with an integration marker and skip them unless an explicit env flag is set:

# Python
import os, pytest

pytestmark_integration = pytest.mark.skipif(
    not os.getenv("RUN_INTEGRATION_TESTS"),
    reason="Set RUN_INTEGRATION_TESTS=1 to run live-network tests"
)

@pytestmark_integration
@pytest.mark.anyio
async def test_fallback_client_can_fetch_block(self):
    ...
// TypeScript
const runIntegration = !!process.env.RUN_INTEGRATION_TESTS;

describe.skipIf(!runIntegration)('TronClientSigner fallback RPC – live', () => {
  it('uses fallback RPC URL and checkBalance succeeds ...', async () => { ... });
});

[MJ-03] Integration test assertion is vacuously true on network failure

Property Value
Severity Major
Category Testing / Correctness
File typescript/packages/x402/src/signers/tronSigner.test.ts : Lines 45–53

Description

TronClientSigner.checkBalance has a broad try/catch that silently swallows errors and returns BigInt(0) on any failure:

// signer.ts (unchanged, context)
} catch (error) {
  console.error(`[TronClientSigner] Failed to check balance: ${error}`);
}
return BigInt(0);

The test asserts only:

expect(typeof balance).toBe('bigint');
expect(balance).toBeGreaterThanOrEqual(BigInt(0));

BigInt(0) >= BigInt(0) is true. Therefore the test passes even if the fallback endpoint is down, times out, or returns garbage — the catch block absorbs the failure and the assertion passes. The test provides false confidence that the fallback endpoint is actually reachable and returning valid data.

Code

const balance = await signer.checkBalance(MAINNET_USDT, 'tron:mainnet');

expect(typeof balance).toBe('bigint');
expect(balance).toBeGreaterThanOrEqual(BigInt(0));  // true even on error path

Recommendation

Assert that the balance is strictly positive (USDT on mainnet for a known active wallet should have a non-zero balance), or spy on console.error and assert it was NOT called, confirming no error occurred during the live call:

// Option A: assert no error was thrown by the catch block
const consoleSpy = vi.spyOn(console, 'error');
const balance = await signer.checkBalance(MAINNET_USDT, 'tron:mainnet');
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();

// Option B: assert a meaningful balance (TEST_WALLET is a known active address)
expect(balance).toBeGreaterThan(BigInt(0));

Minor

[MN-01] Missing logger.info in the mainnet fallback path (Python)

Property Value
Severity Minor
Category Observability
File python/x402/src/bankofai/x402/utils/tron_client.py : Lines 39–47

Description

The non-mainnet (no API key) branch logs both a warning and an info message:

logger.warning("TRON_GRID_API_KEY is not set. Requests may be rate-limited…")
logger.info("Creating AsyncTron client for network=%s", network)  # present here
return AsyncTron(network=network)

The new mainnet fallback branch logs only the warning and omits the info log, so operators cannot distinguish "fallback client created" from "client creation is hanging" without adding their own instrumentation.

Recommendation

Add a matching logger.info call after the provider is set up:

provider = AsyncHTTPProvider(endpoint_uri=TRON_MAINNET_FALLBACK_URL)
logger.info(
    "Creating AsyncTron client with fallback provider for network=%s (%s)",
    network,
    TRON_MAINNET_FALLBACK_URL,
)
return AsyncTron(provider=provider, network=network)

[MN-02] Fallback URL is hardcoded with no environment override

Property Value
Severity Minor
Category Compatibility / Maintainability
File python/x402/src/bankofai/x402/utils/tron_client.py : Line 17; typescript/packages/x402/src/config.ts : Line 80

Description

TRON_MAINNET_FALLBACK_URL / TRON_FALLBACK_RPC_URLS['tron:mainnet'] are compile-time constants with no escape hatch. Users operating in private networks, running a self-hosted node, or subject to geo-restrictions on hptg.bankofai.io have no way to redirect traffic without forking the library or patching the source. An environment variable override would cost one line per language and dramatically improve deployability.

Recommendation

# Python
TRON_MAINNET_FALLBACK_URL = os.getenv(
    "TRON_MAINNET_FALLBACK_RPC_URL", "https://hptg.bankofai.io"
)
// TypeScript
export const TRON_FALLBACK_RPC_URLS: Record<string, string> = {
  'tron:mainnet':
    (typeof process !== 'undefined' && process.env?.TRON_MAINNET_FALLBACK_RPC_URL) ||
    'https://hptg.bankofai.io',
};

Suggestions

[S-01] Document endpoint trust model in JSDoc / docstring

File: typescript/packages/x402/src/config.ts (lines 83–101), python/x402/src/bankofai/x402/utils/tron_client.py (lines 21–33)

Description: The fallback URL is a BankOfAI-operated proxy. All balance checks and contract constant-calls (read operations) made when the API key is absent will flow through this endpoint. This is an important trust boundary for end users. Neither the Python docstring nor the TypeScript JSDoc currently mentions this explicitly.

Suggestion: Add a one-line note to each docstring: "Note: the fallback endpoint is operated by BankOfAI and handles read-only RPC calls (balance queries, constant contract calls). Signing occurs client-side and no private key material is sent to the endpoint." This sets clear expectations for security-conscious users.


[S-02] Consider warning at TronClientSigner construction rather than at every getTronRpcUrl call

File: typescript/packages/x402/src/signers/signer.ts (lines 53–55)

Description: Even after [MJ-01] is fixed with a per-network deduplication set, the warning still originates inside a low-level config helper. Architecturally it makes more sense to emit the warning once in the TronClientSigner constructor (or create() factory), at the point where the operator is configuring a long-lived object, rather than buried in a utility function.

Suggestion:

constructor(wallet: AgentWallet) {
  this.wallet = wallet;
  const apiKey = typeof process !== 'undefined' ? process.env?.TRON_GRID_API_KEY : undefined;
  if (!apiKey) {
    console.warn(
      '[x402] TronClientSigner: TRON_GRID_API_KEY is not set. ' +
      'Mainnet RPC calls will use the BankOfAI fallback endpoint. ' +
      'Set TRON_GRID_API_KEY to use TronGrid.'
    );
  }
}

Positive Observations

Area Observation
Test coverage The unit tests (TestCreateAsyncTronClientFallback, config.test.ts) are thorough: they test with / without API key, with and without the tron: prefix, and cover both mainnet and non-mainnet paths.
Symmetry Python and TypeScript implementations are structurally identical — same constant names, same logic flow, same warning messages — making the codebase easy to reason about across languages.
Non-breaking change TRON_RPC_URLS is preserved unchanged and resolveRpcUrl still returns the same values when TRON_GRID_API_KEY is set, so callers relying on the old behaviour see no difference.
Clean refactor in signer.ts Replacing the direct map lookup TRON_RPC_URLS[network] with getTronRpcUrl(network) is a one-line change with no risk of regression; the caching in tronWebInstances still works correctly because the cache key is the resolved URL, not the network name.
Guard against process being undefined Both getTronRpcUrl and createTronWeb defensively check typeof process !== 'undefined' before reading process.env, making the code safe in browser/edge runtime environments.
User-friendly warning messages Warning text in both languages explicitly tells the operator what env var to set (TRON_GRID_API_KEY) and what endpoint is being used, making the fallback fully transparent.

Checklist Results

Category Items Checked Pass Fail N/A Notes
Correctness 8 8 0 0 Logic is correct; prefix stripping, API key branch, fallback branch all work
Security 9 8 0 1 No hardcoded secrets; fallback endpoint documented in warning; path-traversal N/A
Performance 7 6 1 0 console.warn on every call (MJ-01)
Code Quality 10 9 1 0 Missing logger.info in one branch (MN-01); otherwise clean
Testing 7 4 3 0 No skip guards for live tests (MJ-02); vacuous assertion (MJ-03)
Documentation 6 5 0 1 Good docstrings; trust model not documented (S-01, suggestion only)
Compatibility 5 5 0 0 Backward compatible; no breaking changes
Observability 4 3 1 0 Missing info log in mainnet fallback path (MN-01)

Disclaimer

This is an automated code review. It supplements but does not replace human review. The reviewer analyzed only the diff between main and feature/fallback-rpc-no-api-key. Runtime behaviour, integration testing, and deployment impact are not covered.


Report generated by Code Review Skill v1.0.0
Date: 2026-03-30

- Emit fallback warning once per network using a module-level Set
- Assert console.error was not called in TS signer test to catch silent failures
- Add logger.info after fallback provider creation in Python

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Code Review Report

Project: x402 (BankOfAI)
PR: mainfeature/fallback-rpc-no-api-key
Review Date: 2026-03-30
Reviewer: AI Code Reviewer (Code Review Skill v1.0.0)


PR Overview

Branch Information

Property Value
From Branch main
To Branch feature/fallback-rpc-no-api-key
Commits 3
Files Changed 6
Lines Added +275
Lines Removed -6

Commit History

Hash Message
b56955d fix: deduplicate console.warn, add console.error spy, add logger.info
94faeb0 fix: address code review feedback on fallback RPC
52d3f7f feat: use fallback RPC URL for TRON mainnet when TRON_GRID_API_KEY is not set

Review Summary

Verdict

Verdict: Request Changes

Findings at a Glance

Critical Major Minor Suggestion
Count 0 3 4 2

Summary

This PR introduces a fallback RPC endpoint (https://hptg.bankofai.io) for TRON mainnet when TRON_GRID_API_KEY is not set, covering both the Python (tron_client.py) and TypeScript (config.ts / signer.ts) sides of the SDK. The intent is good — improving out-of-the-box usability for users who have not yet configured TronGrid — and the implementation is generally clean and well-structured.

However, three major issues require attention before merge. First, the fallback silently routes live mainnet RPC traffic (including signed transaction broadcasts) through a proprietary BankOfAI-controlled endpoint; users installing this SDK may not realize their mainnet calls bypass TronGrid and go to an undocumented third-party URL. Second, the test suite includes live-network integration tests (Python test_fallback_client_can_fetch_block and TypeScript tronSigner.test.ts) that make real HTTP calls against the fallback endpoint — these will fail in offline CI environments and make the suite non-deterministic. Third, the TypeScript module-level _warnedNetworks Set is mutable global state that persists across test cases and can cause order-dependent test behavior.

Four minor issues round out the review: a misplaced JSDoc comment, the hardcoded fallback URL not being configurable via environment variable, inconsistent warning-deduplication between Python and TypeScript, and a missing timeout/header for TronGrid API key injection on the fallback path in TypeScript.


Change Summary

1. Python — Fallback RPC for TRON mainnet (tron_client.py)

File Change Type Description
python/x402/src/bankofai/x402/utils/tron_client.py Modified Adds TRON_MAINNET_FALLBACK_URL constant; extends create_async_tron_client to use AsyncHTTPProvider(endpoint_uri=TRON_MAINNET_FALLBACK_URL) for mainnet when no API key is present

Purpose: Make create_async_tron_client("mainnet") work without a TronGrid API key by routing through a BankOfAI-controlled fallback node.


2. Python — New test file for fallback behavior

File Change Type Description
python/x402/tests/utils/test_tron_client_factory.py Added Unit tests (with mocks) and a live functional test for the fallback RPC selection logic

Purpose: Verify that the correct provider/URL is chosen based on the TRON_GRID_API_KEY environment variable and network name.


3. TypeScript — Fallback RPC exported constant + getTronRpcUrl function (config.ts)

File Change Type Description
typescript/packages/x402/src/config.ts Modified Adds TRON_FALLBACK_RPC_URLS map, getTronRpcUrl() function with per-network warn-once logic, and updates resolveRpcUrl() to route TRON networks through getTronRpcUrl()

Purpose: Mirror the Python fallback logic in TypeScript; deduplicates warning messages using a module-level Set.


4. TypeScript — signer.ts wired to getTronRpcUrl

File Change Type Description
typescript/packages/x402/src/signers/signer.ts Modified Replaces TRON_RPC_URLS[network] with getTronRpcUrl(network) in getTronWeb()

Purpose: Ensure TronClientSigner uses the fallback URL when no API key is set.


5. TypeScript — New test files

File Change Type Description
typescript/packages/x402/src/config.test.ts Added Unit tests for getTronRpcUrl and resolveRpcUrl fallback/API-key branching
typescript/packages/x402/src/signers/tronSigner.test.ts Added Integration test that verifies checkBalance calls succeed via the fallback RPC

Purpose: Test coverage for the new routing logic.


Detailed Findings


Major

[MJ-01] Mainnet RPC Traffic — Including Signed Transactions — Is Silently Routed to a Proprietary Third-Party Endpoint

Property Value
Severity Major
Category Security / Correctness
File (Python) python/x402/src/bankofai/x402/utils/tron_client.py : Lines 17, 40–52
File (TypeScript) typescript/packages/x402/src/config.ts : Lines 79–81, 89–106

Description

The hardcoded fallback https://hptg.bankofai.io is a BankOfAI-operated node. When TRON_GRID_API_KEY is absent, all mainnet RPC calls are directed there — including triggerConstantContract (balance/allowance queries), triggerSmartContract (approve transactions), and critically trx.sendRawTransaction (raw signed transaction broadcast). An SDK consumer who simply omits the API key may unknowingly:

  • Send signed (ready-to-broadcast) transactions to a non-TronGrid endpoint
  • Receive manipulated contract query results (e.g., falsely elevated balance or allowance)
  • Have their signed transaction logged or selectively dropped without error

The logger.warning / console.warn message is the only disclosure, and it is easy to miss in production log streams.

Code

# Python – tron_client.py
TRON_MAINNET_FALLBACK_URL = "https://hptg.bankofai.io"
...
if network == "mainnet":
    provider = AsyncHTTPProvider(endpoint_uri=TRON_MAINNET_FALLBACK_URL)
    return AsyncTron(provider=provider, network=network)
// TypeScript – config.ts
export const TRON_FALLBACK_RPC_URLS: Record<string, string> = {
  'tron:mainnet': 'https://hptg.bankofai.io',
};

Recommendation

  1. Document the fallback endpoint prominently in the README and CHANGELOG, including trust implications.
  2. Consider adding an opt-in environment variable (e.g., TRON_ALLOW_FALLBACK_RPC=true) so the behaviour is explicit rather than silent.
  3. If a proprietary endpoint is to remain the default, at minimum make the TRON_FALLBACK_RPC_URL overridable via environment variable so users can substitute their own node without code changes.
# Example: environment-variable-overridable fallback
TRON_MAINNET_FALLBACK_URL = os.getenv(
    "TRON_MAINNET_FALLBACK_URL", "https://hptg.bankofai.io"
)

[MJ-02] Live-Network Integration Tests in the Unit Test Suite Will Break Offline CI

Property Value
Severity Major
Category Testing
File (Python) python/x402/tests/utils/test_tron_client_factory.py : Lines 71–90
File (TypeScript) typescript/packages/x402/src/signers/tronSigner.test.ts : Lines 37–58

Description

Two tests make real HTTP calls to https://hptg.bankofai.io:

  • Python TestFallbackClientFunctional::test_fallback_client_can_fetch_block — calls client.get_latest_block() over the live network.
  • TypeScript tronSigner.test.ts — calls signer.checkBalance(MAINNET_USDT, 'tron:mainnet') which internally fires triggerConstantContract to the live fallback endpoint.

Neither test has a guard for CI (no pytest.mark.skipif, no pytest.mark.network, no vi.mock of the HTTP layer). This means:

  • The CI pipeline will fail whenever the fallback endpoint is unavailable or rate-limits the runner.
  • Test results are non-deterministic (network latency, block availability).
  • The unit test suite now has an implicit external dependency with no documented SLA.

Code

# test_tron_client_factory.py – live network call
async def test_fallback_client_can_fetch_block(self):
    client = create_async_tron_client("tron:mainnet")
    async with client:
        block = await client.get_latest_block()   # real HTTP call
        assert block["block_header"]["raw_data"]["number"] > 0
// tronSigner.test.ts – live network call
const balance = await signer.checkBalance(MAINNET_USDT, 'tron:mainnet');  // real HTTP call
expect(typeof balance).toBe('bigint');

Recommendation

Move live-network tests into a separate file/directory (e.g., tests/integration/) and gate them with a marker or environment variable:

# Python: mark the test class
@pytest.mark.integration
@pytest.mark.anyio
@patch.dict("os.environ", {}, clear=True)
async def test_fallback_client_can_fetch_block(self):
    ...
// TypeScript: skip unless integration flag is set
it.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
  'uses fallback RPC URL and checkBalance succeeds on tron:mainnet',
  async () => { ... }
);

[MJ-03] Module-Level Mutable _warnedNetworks Set Causes Non-Deterministic Test Behavior

Property Value
Severity Major
Category Testing / Code Quality
File typescript/packages/x402/src/config.ts : Lines 87–100

Description

The _warnedNetworks Set is module-level global state:

const _warnedNetworks = new Set<string>();

Because ES modules are singletons in a Node.js process, this Set is shared across all test files that import config.ts within a single Vitest run. Consequences:

  1. Warning assertion tests are order-dependent: the first test that calls getTronRpcUrl('tron:mainnet') without an API key fires the console.warn; all subsequent calls (in other tests or other test files) skip the warning. Any test that asserts console.warn was (or was not) called can pass or fail depending on test execution order.
  2. Warning state cannot be reset between tests: _warnedNetworks is unexported and not reassignable, so there is no clean teardown path short of vi.resetModules().
  3. The config.test.ts file does not reset this state between its beforeEach/afterEach hooks, making the "returns fallback URL for mainnet when TRON_GRID_API_KEY is not set" test potentially non-idempotent on repeated runs.

Code

// config.ts
const _warnedNetworks = new Set<string>();   // ← module-level, persists between tests

export function getTronRpcUrl(network: string): string | undefined {
  ...
  if (!_warnedNetworks.has(network)) {
    _warnedNetworks.add(network);
    console.warn(...);
  }
  ...
}

Recommendation

Export a _resetWarnedNetworks function (test-only) or replace the module-level Set with a closure that can be injected/reset:

// Option A: export a reset helper (test-only)
export function _resetWarnedNetworks_forTestingOnly(): void {
  _warnedNetworks.clear();
}

Then call it in afterEach in config.test.ts:

import { _resetWarnedNetworks_forTestingOnly } from './config.js';
afterEach(() => _resetWarnedNetworks_forTestingOnly());

Alternatively, use vi.resetModules() + dynamic import() in tests that need a fresh module state.


Minor

[MN-01] JSDoc Comment Is Misplaced — Documents _warnedNetworks Instead of getTronRpcUrl

Property Value
Severity Minor
Category Documentation
File typescript/packages/x402/src/config.ts : Lines 83–87

Description

The JSDoc comment for getTronRpcUrl is positioned before the _warnedNetworks variable declaration rather than directly above the function. As a result, IDEs and documentation generators will associate the comment with _warnedNetworks, not the function.

Code

/**
 * Get the appropriate TRON RPC URL for a network.
 * Uses fallback URLs when TRON_GRID_API_KEY is not set.
 */
const _warnedNetworks = new Set<string>();   // ← comment incorrectly attached here

export function getTronRpcUrl(network: string): string | undefined {

Recommendation

Move the _warnedNetworks declaration above the JSDoc block, or move the JSDoc to directly precede the function:

const _warnedNetworks = new Set<string>();

/**
 * Get the appropriate TRON RPC URL for a network.
 * Uses fallback URLs when TRON_GRID_API_KEY is not set.
 */
export function getTronRpcUrl(network: string): string | undefined {

[MN-02] Fallback URL Is Hardcoded with No Environment-Variable Override

Property Value
Severity Minor
Category Compatibility / Code Quality
File (Python) python/x402/src/bankofai/x402/utils/tron_client.py : Line 17
File (TypeScript) typescript/packages/x402/src/config.ts : Lines 79–81

Description

Both Python and TypeScript hardcode the fallback URL as a constant with no way to override it without modifying source. Users running their own TRON full node, or using a different public RPC, cannot redirect the fallback without forking the library.

Recommendation

Read from an environment variable with the hardcoded value as default:

TRON_MAINNET_FALLBACK_URL = os.getenv("TRON_MAINNET_FALLBACK_URL", "https://hptg.bankofai.io")
export const TRON_FALLBACK_RPC_URLS: Record<string, string> = {
  'tron:mainnet': process.env.TRON_MAINNET_FALLBACK_URL ?? 'https://hptg.bankofai.io',
};

[MN-03] Warning Deduplication Is Inconsistent Between Python and TypeScript

Property Value
Severity Minor
Category Code Quality
File (Python) python/x402/src/bankofai/x402/utils/tron_client.py : Lines 54–57
File (TypeScript) typescript/packages/x402/src/config.ts : Lines 93–100

Description

The TypeScript implementation deduplicates warnings per-network using _warnedNetworks, but the Python implementation emits the "TRON_GRID_API_KEY is not set. Requests may be rate-limited..." warning every single time create_async_tron_client is called for non-mainnet networks (e.g., nile, shasta). Heavy-use code paths will emit this warning on every client creation.

Additionally, the mainnet fallback warning in Python is also not deduplicated — it fires every time create_async_tron_client("mainnet") is called.

Recommendation

Apply a module-level warned flag or set in Python, consistent with the TypeScript approach:

_warned_networks: set[str] = set()

def create_async_tron_client(network: str) -> Any:
    ...
    if not api_key:
        if network == "mainnet":
            if "mainnet" not in _warned_networks:
                _warned_networks.add("mainnet")
                logger.warning("TRON_GRID_API_KEY is not set. Mainnet RPC calls will be routed to %s. ...", ...)
            ...

[MN-04] createTronWeb Does Not Pass TRON_GRID_API_KEY Header When Using Fallback URL

Property Value
Severity Minor
Category Correctness
File typescript/packages/x402/src/signers/signer.ts : Lines 112–118

Description

createTronWeb in signer.ts injects TRON-PRO-API-KEY into the headers when TRON_GRID_API_KEY is set:

private createTronWeb(fullHost: string): TronWeb {
    const apiKey = typeof process !== 'undefined' ? process.env?.TRON_GRID_API_KEY : undefined;
    const headers = apiKey ? { 'TRON-PRO-API-KEY': apiKey } : undefined;
    ...
    return new TronWebClass({ fullHost, privateKey: dummyKey, headers }) ...
}

This is correct. However, when fullHost is the fallback URL (https://hptg.bankofai.io) and TRON_GRID_API_KEY is absent, headers is undefined, which is fine. But if a user later sets TRON_GRID_API_KEY mid-session, the TronWeb instance for the fallback host is still cached in tronWebInstances under the key https://hptg.bankofai.io. The cache key is the host URL, so calling getTronWeb('tron:mainnet') after the API key is set would return a stale instance pointing to the fallback endpoint without the API key header.

This is an edge case (env vars do not typically change at runtime), but the caching strategy being based on host URL rather than (host, apiKey) tuple could lead to subtle bugs.

Recommendation

Include the API key in the cache key, or invalidate the cache when the API key changes:

private getTronWeb(network?: string): TronWeb {
  const host = network ? getTronRpcUrl(network) : undefined;
  const apiKey = typeof process !== 'undefined' ? process.env?.TRON_GRID_API_KEY : undefined;
  const key = `${host ?? '__default__'}::${apiKey ?? ''}`;
  ...
}

Suggestions

[S-01] Consider Adding a Request Timeout for the Fallback Endpoint

File: python/x402/src/bankofai/x402/utils/tron_client.py, typescript/packages/x402/src/signers/signer.ts

Description: Neither implementation configures an explicit HTTP timeout for the fallback endpoint. If https://hptg.bankofai.io becomes unresponsive (e.g., during maintenance), RPC calls will hang indefinitely, potentially blocking the caller's entire async task.

Suggestion: Configure a reasonable timeout (e.g., 10 seconds) on the AsyncHTTPProvider in Python, and pass a timeout option in the TronWeb constructor for TypeScript. Add a note in the docstring that the fallback endpoint carries different availability guarantees than TronGrid.


[S-02] Document the Fallback Endpoint in CHANGELOG and README

File: CHANGELOG.md, README.md

Description: The introduction of a default fallback to a BankOfAI-operated RPC node is a meaningful behavioral change for users who currently run without TRON_GRID_API_KEY (previously they would get rate-limit errors from TronGrid; now they silently succeed via the fallback). This change should be documented in the CHANGELOG under a clear heading (e.g., "Behavior Change") and in the README under the TRON configuration section so users can make an informed decision.

Suggestion: Add an entry to CHANGELOG.md describing the new fallback behavior, its URL, and the opt-out/override path.


Positive Observations

Area Observation
Warn-once deduplication (TS) The _warnedNetworks Set approach in TypeScript is a thoughtful UX touch — it prevents log spam in long-running processes without requiring a full logger framework.
Test coverage breadth The PR adds 218 lines of tests across three new test files covering both mock-based unit tests and functional assertions, demonstrating solid test discipline.
Consistent behavior between Python and TS Both sides of the SDK implement the same fallback logic using the same URL and the same network == "mainnet" branching, keeping the Python and TypeScript SDKs behaviorally aligned.
Clear docstring update The updated docstring in create_async_tron_client clearly describes the three-way branching (API key set, no key + mainnet, no key + other) rather than a single vague sentence.
Correct tron: prefix stripping The Python test test_mainnet_with_tron_prefix_uses_fallback explicitly covers the tron:mainnetmainnet prefix normalization, catching a subtle edge case.
No breaking API changes The TypeScript change from TRON_RPC_URLS[network] to getTronRpcUrl(network) in signer.ts is transparent to callers; the public interface of TronClientSigner is unchanged.

Checklist Results

Category Items Checked Pass Fail N/A Notes
Correctness 8 6 1 1 TronWeb instance cache key doesn't account for API key changes [MN-04]
Security 10 8 1 1 Fallback routes mainnet signed tx to proprietary endpoint [MJ-01]
Performance 7 6 0 1 No request timeout for fallback endpoint [S-01]
Code Quality 10 7 2 1 Misplaced JSDoc [MN-01]; module-level mutable state [MJ-03]
Testing 7 4 2 1 Live network tests in unit suite [MJ-02]; non-deterministic warning assertions [MJ-03]
Documentation 6 3 2 1 JSDoc comment misplaced [MN-01]; CHANGELOG not updated [S-02]
Compatibility 5 4 1 0 No env-var override for fallback URL [MN-02]
Observability 4 3 1 0 Warning deduplication inconsistent between Python and TS [MN-03]

Disclaimer

This is an automated code review. It supplements but does not replace human review. The reviewer analyzed only the diff between origin/main and origin/feature/fallback-rpc-no-api-key. Runtime behavior, integration testing, and deployment impact beyond the described findings are not covered.


Report generated by Code Review Skill v1.0.0
Date: 2026-03-30

@Hades-Ye Hades-Ye merged commit d6c45cc into main Mar 30, 2026
9 checks passed
@Hades-Ye Hades-Ye deleted the feature/fallback-rpc-no-api-key branch March 30, 2026 04:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant