From 6723a8ce3ad94d649fe0787e1bada1c5342d3049 Mon Sep 17 00:00:00 2001 From: ahmadghoniem Date: Wed, 22 Apr 2026 11:15:46 +0200 Subject: [PATCH] x402: fallback to on-chain gateway balance when API balance is stale --- src/omniclaw/agent/routes.py | 21 +++++++++--- tests/test_x402_sdk_adapter.py | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/omniclaw/agent/routes.py b/src/omniclaw/agent/routes.py index a9711f4..6a73dab 100644 --- a/src/omniclaw/agent/routes.py +++ b/src/omniclaw/agent/routes.py @@ -92,11 +92,22 @@ async def _choose_x402_route( gateway_available_balance = balance.formatted_available required_atomic = int(selected_gateway_kind.amount_atomic) gateway_ready = balance.available >= required_atomic - gateway_reason = ( - "Gateway balance is sufficient for GatewayWalletBatched" - if gateway_ready - else "Gateway balance is below the required amount" - ) + if gateway_ready: + gateway_reason = "Gateway balance is sufficient for GatewayWalletBatched" + else: + # Fallback to direct on-chain balance when API-reported balance is stale/lagging. + try: + onchain_balance = await client.get_gateway_onchain_balance(wallet_id) + if onchain_balance.available >= required_atomic: + gateway_available_balance = onchain_balance.formatted_available + gateway_ready = True + gateway_reason = ( + "Gateway on-chain balance is sufficient (API balance appears stale)" + ) + else: + gateway_reason = "Gateway balance is below the required amount" + except Exception: + gateway_reason = "Gateway balance is below the required amount" except Exception as exc: gateway_ready = False gateway_reason = f"Gateway balance check failed: {exc}" diff --git a/tests/test_x402_sdk_adapter.py b/tests/test_x402_sdk_adapter.py index 9054a63..34f9941 100644 --- a/tests/test_x402_sdk_adapter.py +++ b/tests/test_x402_sdk_adapter.py @@ -346,6 +346,12 @@ async def test_choose_x402_route_prefers_exact_when_gateway_is_unfunded(): formatted_available="0.00", ) ), + get_gateway_onchain_balance=AsyncMock( + return_value=SimpleNamespace( + available=0, + formatted_available="0.00", + ) + ), ) x402_adapter = SimpleNamespace( _resolve_agent_network=lambda wallet_id, destination_chain: "eip155:84532" @@ -385,6 +391,12 @@ async def test_choose_x402_route_keeps_gateway_when_it_is_the_only_option(): formatted_available="0.00", ) ), + get_gateway_onchain_balance=AsyncMock( + return_value=SimpleNamespace( + available=0, + formatted_available="0.00", + ) + ), ) x402_adapter = SimpleNamespace( _resolve_agent_network=lambda wallet_id, destination_chain: "eip155:84532" @@ -403,6 +415,52 @@ async def test_choose_x402_route_keeps_gateway_when_it_is_the_only_option(): assert route["gateway_ready"] is False +@pytest.mark.asyncio +async def test_choose_x402_route_uses_onchain_fallback_when_api_balance_is_stale(): + gateway_kind = SimpleNamespace( + amount_atomic=250000, + is_gateway_batched=True, + get_amount_usdc=lambda: Decimal("0.25"), + ) + requirements = SimpleNamespace( + select_preferred_kind=lambda *, prefer_gateway, source_network: ( + gateway_kind if prefer_gateway else None + ) + ) + client = SimpleNamespace( + _nano_adapter=object(), + get_gateway_balance=AsyncMock( + return_value=SimpleNamespace( + available=0, + formatted_available="0.00", + ) + ), + get_gateway_onchain_balance=AsyncMock( + return_value=SimpleNamespace( + available=300000, + formatted_available="0.30", + ) + ), + ) + x402_adapter = SimpleNamespace( + _resolve_agent_network=lambda wallet_id, destination_chain: "eip155:84532" + ) + + route = await _choose_x402_route( + client=client, + wallet_id="buyer-wallet", + x402_adapter=x402_adapter, + requirements=requirements, + ) + + assert route["selected_route"] == "nanopayment" + assert route["payment_source"] == "gateway_balance" + assert route["selected_kind"] is gateway_kind + assert route["gateway_ready"] is True + assert route["gateway_available_balance"] == "0.30" + assert route["gateway_reason"] == "Gateway on-chain balance is sufficient (API balance appears stale)" + + @pytest.mark.asyncio async def test_pay_route_inspects_url_even_when_amount_is_supplied( monkeypatch: pytest.MonkeyPatch,