feat: fallback RPC for TRON mainnet without API key#60
Conversation
… 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>
Code Review ReportProject: x402 (BankOfAI x402 Payment Protocol SDK) PR OverviewBranch Information
Commit History
Review SummaryVerdict
Findings at a Glance
SummaryThis PR introduces a fallback RPC endpoint ( 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 Change Summary1. Python fallback —
|
| 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
WARNINGor higher log level (Python logsINFO; 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:
- 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. - Integrity: a compromised or malicious endpoint could return crafted RPC responses — falsified balances, altered contract ABIs, fabricated
triggerConstantContractresults — that mislead client-side payment logic without triggering any visible error. - Availability as a single point of failure: there is no multi-endpoint fallback, health-check, or retry logic. If
hptg.bankofai.iois unreachable, all no-key mainnet users are broken in exactly the same way as before. - 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:
- Escalate the log level to
WARNING(Python) and emit an equivalentconsole.warn(TypeScript) that explicitly names the endpoint and notes that all RPC calls will be routed there. - Respect a user-configured override: check
TRON_RPC_URLorTRON_FALLBACK_RPC_URLenv var first; only use the hardcoded URL if that is also absent. - Document the behaviour: add a note in the SDK README and in the docstring of
create_async_tron_client/getTronRpcUrlthat the fallback endpoint is operated by BankOfAI, is subject to rate limits and availability, and how to override it. - 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 RPC→checkBalancetest (TypeScript) — callstriggerConstantContracton 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 assertionRecommendation
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:17Recommendation
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 attributeRecommendation
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:mainnet → mainnet 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>
Code Review ReportProject: x402 (BankOfAI) PR OverviewBranch Information
Commit History
Review SummaryVerdict
Findings at a Glance
SummaryThis PR introduces a fallback RPC endpoint ( However, there are three issues that should be resolved before merging. First, the TypeScript Change Summary1 · Python — Fallback RPC logic & tests
Purpose: When 2 · TypeScript —
|
| 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_blockcallsclient.get_latest_block()over the network. - TypeScript
TronClientSigner fallback RPCcallssigner.checkBalance(MAINNET_USDT, 'tron:mainnet'), which in turn issues atriggerConstantContractcall 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 callRecommendation
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 pathRecommendation
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>
Code Review ReportProject: x402 (BankOfAI) PR OverviewBranch Information
Commit History
Review SummaryVerdict
Findings at a Glance
SummaryThis PR introduces a fallback RPC endpoint ( 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 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 Summary1. Python — Fallback RPC for TRON mainnet (
|
| 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
- Document the fallback endpoint prominently in the README and CHANGELOG, including trust implications.
- Consider adding an opt-in environment variable (e.g.,
TRON_ALLOW_FALLBACK_RPC=true) so the behaviour is explicit rather than silent. - If a proprietary endpoint is to remain the default, at minimum make the
TRON_FALLBACK_RPC_URLoverridable 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— callsclient.get_latest_block()over the live network. - TypeScript
tronSigner.test.ts— callssigner.checkBalance(MAINNET_USDT, 'tron:mainnet')which internally firestriggerConstantContractto 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:
- Warning assertion tests are order-dependent: the first test that calls
getTronRpcUrl('tron:mainnet')without an API key fires theconsole.warn; all subsequent calls (in other tests or other test files) skip the warning. Any test that assertsconsole.warnwas (or was not) called can pass or fail depending on test execution order. - Warning state cannot be reset between tests:
_warnedNetworksis unexported and not reassignable, so there is no clean teardown path short ofvi.resetModules(). - The
config.test.tsfile does not reset this state between itsbeforeEach/afterEachhooks, 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:mainnet → mainnet 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
Summary
TRON_GRID_API_KEYis not set, the TRON signer now useshttps://hptg.bankofai.ioas the mainnet RPC instead of the rate-limited TronGrid endpointTRON_GRID_API_KEYis set, behavior is unchanged (uses TronGrid with the API key)🤖 Generated with Claude Code