From 639a23df82eaaded446b2329f06c4e34a0ebde89 Mon Sep 17 00:00:00 2001 From: spalen0 Date: Tue, 31 Mar 2026 13:19:35 +0200 Subject: [PATCH] feat(safe): halve API calls and rotate between two API keys - Remove redundant get_last_executed_nonce() API call; use cached nonce from file instead (1 call per safe instead of 2) - Add round-robin rotation between SAFE_API_KEY and SAFE_API_KEY_2 - Add SAFE_API_KEY_2 to workflow and .env.example With 23 safes running every 10 minutes, this drops monthly usage from ~199k to ~99k calls, split ~50k per key (within Safe free tier limit). Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 3 ++- .github/workflows/_run-monitoring.yml | 1 + safe/main.py | 36 +++++++++++---------------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index 2a0fd59..84e5e9d 100644 --- a/.env.example +++ b/.env.example @@ -36,8 +36,9 @@ AAVE_ALERT_THRESHOLD=0.95 AAVE_CRITICAL_THRESHOLD=0.98 AAVE_ENABLE_NOTIFICATIONS=true -# Safe +# Safe (two keys rotated to stay within API rate limits) SAFE_API_KEY=your-api-key +SAFE_API_KEY_2=your-second-api-key # Tenderly (for transaction simulation) TENDERLY_API_KEY=your-tenderly-api-key diff --git a/.github/workflows/_run-monitoring.yml b/.github/workflows/_run-monitoring.yml index 3e56861..c219f78 100644 --- a/.github/workflows/_run-monitoring.yml +++ b/.github/workflows/_run-monitoring.yml @@ -121,6 +121,7 @@ env: COINGECKO_API_KEY: ${{ secrets.COINGECKO_API_KEY }} TENDERLY_API_KEY: ${{ secrets.TENDERLY_API_KEY }} SAFE_API_KEY: ${{ secrets.SAFE_API_KEY }} + SAFE_API_KEY_2: ${{ secrets.SAFE_API_KEY_2 }} LLM_API_KEY: ${{ secrets.LLM_API_KEY }} LLM_MODEL: ${{ secrets.LLM_MODEL }} LLM_PROVIDER: ${{ secrets.LLM_PROVIDER }} diff --git a/safe/main.py b/safe/main.py index 24e5d54..3e7a9f2 100644 --- a/safe/main.py +++ b/safe/main.py @@ -1,3 +1,4 @@ +import itertools import os import time @@ -21,6 +22,12 @@ provider_url_mainnet = os.getenv("PROVIDER_URL_MAINNET") provider_url_arb = os.getenv("PROVIDER_URL_ARBITRUM") +# Round-robin iterator over available Safe API keys. +_api_keys: list[str] = [k for k in [os.getenv("SAFE_API_KEY"), os.getenv("SAFE_API_KEY_2")] if k] +if not _api_keys: + raise ValueError("At least one SAFE_API_KEY must be set.") +_api_key_cycle = itertools.cycle(_api_keys) + safe_address_network_prefix = { "mainnet": "eth", "arbitrum-main": "arb1", @@ -188,9 +195,7 @@ def get_safe_transactions( if executed is not None: params["executed"] = str(executed).lower() - api_key = os.getenv("SAFE_API_KEY") - if not api_key: - raise ValueError("SAFE_API_KEY environment variable not set.") + api_key = next(_api_key_cycle) headers = { "Authorization": f"Bearer {api_key}", @@ -230,20 +235,11 @@ def get_safe_transactions( return [] -def get_last_executed_nonce(safe_address: str, network_name: str) -> int: - executed_txs = get_safe_transactions(safe_address, network_name, executed=True, limit=1) - if executed_txs: - return int(executed_txs[0]["nonce"]) - return -1 # Return -1 if no executed transactions found - - -def get_pending_transactions_after_last_executed(safe_address: str, network_name: str) -> list[dict]: - last_executed_nonce = get_last_executed_nonce(safe_address, network_name) +def get_pending_transactions(safe_address: str, network_name: str) -> list[dict]: + """Fetch pending transactions with nonce higher than the last cached nonce.""" + last_cached_nonce = get_last_executed_nonce_from_file(safe_address) pending_txs = get_safe_transactions(safe_address, network_name, executed=False) - - if pending_txs: - return [tx for tx in pending_txs if int(tx["nonce"]) > last_executed_nonce] - return [] + return [tx for tx in pending_txs if int(tx["nonce"]) > last_cached_nonce] def get_safe_url(safe_address: str, network_name: str) -> str: @@ -251,15 +247,11 @@ def get_safe_url(safe_address: str, network_name: str) -> str: def check_for_pending_transactions(safe_address: str, network_name: str, protocol: str) -> None: - pending_transactions = get_pending_transactions_after_last_executed(safe_address, network_name) + pending_transactions = get_pending_transactions(safe_address, network_name) if pending_transactions: for tx in pending_transactions: nonce = int(tx["nonce"]) - # skip tx if the nonce is already processed - if nonce <= get_last_executed_nonce_from_file(safe_address): - logger.info("Skipping tx with nonce %s as it is already processed.", nonce) - continue target_contract = tx["to"] @@ -355,7 +347,7 @@ def main(): logger.info("Running for %s on %s", safe[0], safe[1]) last_api_call_time, request_counter = check_api_limit(last_api_call_time, request_counter) run_for_network(safe[1], safe[2], safe[0]) - request_counter += 2 + request_counter += 1 if __name__ == "__main__":