alloc-context 0.2.4__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. {alloc_context-0.2.4 → alloc_context-0.2.6}/PKG-INFO +11 -10
  2. {alloc_context-0.2.4 → alloc_context-0.2.6}/README.md +8 -7
  3. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloc_context.egg-info/PKG-INFO +11 -10
  4. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloc_context.egg-info/SOURCES.txt +9 -0
  5. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/__init__.py +1 -1
  6. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/config.py +36 -0
  7. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/asset_registry.py +2 -0
  8. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/exchange/live.py +26 -1
  9. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/exchange/types.py +1 -1
  10. alloc_context-0.2.6/alloccontext/ingest/wallet/__init__.py +6 -0
  11. alloc_context-0.2.6/alloccontext/ingest/wallet/alchemy.py +191 -0
  12. alloc_context-0.2.6/alloccontext/ingest/wallet/chains.py +46 -0
  13. alloc_context-0.2.6/alloccontext/ingest/wallet/curated_tokens.py +66 -0
  14. alloc_context-0.2.6/alloccontext/ingest/wallet/etherscan.py +131 -0
  15. alloc_context-0.2.6/alloccontext/ingest/wallet/portfolio.py +315 -0
  16. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/bazaar.py +8 -5
  17. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/handlers.py +17 -12
  18. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/http.py +2 -0
  19. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/instructions.py +3 -2
  20. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/server.py +6 -3
  21. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/tool_catalog.py +18 -13
  22. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/tool_fields.py +17 -3
  23. {alloc_context-0.2.4 → alloc_context-0.2.6}/pyproject.toml +4 -2
  24. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_deploy.py +2 -0
  25. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_x402.py +28 -0
  26. alloc_context-0.2.6/tests/test_wallet_alchemy.py +98 -0
  27. alloc_context-0.2.6/tests/test_wallet_etherscan.py +47 -0
  28. alloc_context-0.2.6/tests/test_wallet_portfolio.py +139 -0
  29. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_x402_bazaar_dynamic.py +8 -1
  30. {alloc_context-0.2.4 → alloc_context-0.2.6}/LICENSE +0 -0
  31. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloc_context.egg-info/dependency_links.txt +0 -0
  32. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloc_context.egg-info/entry_points.txt +0 -0
  33. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloc_context.egg-info/requires.txt +0 -0
  34. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloc_context.egg-info/top_level.txt +0 -0
  35. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/__main__.py +0 -0
  36. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/constants.py +0 -0
  37. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/horizon.py +0 -0
  38. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/__init__.py +0 -0
  39. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/alt_quote_registry.py +0 -0
  40. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/alt_quote_store.py +0 -0
  41. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/alt_quotes.py +0 -0
  42. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/cf_benchmarks.py +0 -0
  43. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/cf_history.py +0 -0
  44. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/coinbase_client.py +0 -0
  45. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/coinbase_portfolio.py +0 -0
  46. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/coingecko.py +0 -0
  47. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/coinmarketcap.py +0 -0
  48. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/env_keys.py +0 -0
  49. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/etf_flows.py +0 -0
  50. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/exchange/__init__.py +0 -0
  51. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/exchange/coinbase_adapter.py +0 -0
  52. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/exchange/kraken_adapter.py +0 -0
  53. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/exchange/portfolio.py +0 -0
  54. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/exchange/registry.py +0 -0
  55. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/exchange_http.py +0 -0
  56. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/fear_greed.py +0 -0
  57. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/fred.py +0 -0
  58. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/http_errors.py +0 -0
  59. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/kalshi.py +0 -0
  60. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/kalshi_api.py +0 -0
  61. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/kalshi_client.py +0 -0
  62. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/kalshi_files.py +0 -0
  63. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/kalshi_state.py +0 -0
  64. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/kraken_client.py +0 -0
  65. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/kraken_portfolio.py +0 -0
  66. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/macro_calendar.py +0 -0
  67. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/macro_normalize.py +0 -0
  68. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/market_snapshots.py +0 -0
  69. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/outcome.py +0 -0
  70. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/parse_helpers.py +0 -0
  71. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/portfolio_holdings.py +0 -0
  72. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/quote_resolver.py +0 -0
  73. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/ingest/runner.py +0 -0
  74. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/integrations/__init__.py +0 -0
  75. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/integrations/langchain.py +0 -0
  76. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/__init__.py +0 -0
  77. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/assets.py +0 -0
  78. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/bridge.py +0 -0
  79. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/bridge_portfolio.py +0 -0
  80. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/contracts.py +0 -0
  81. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/glama.py +0 -0
  82. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/payer.py +0 -0
  83. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/payment_middleware.py +0 -0
  84. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/setup.py +0 -0
  85. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/staleness.py +0 -0
  86. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/upstream.py +0 -0
  87. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/validation.py +0 -0
  88. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/x402_bazaar_dynamic.py +0 -0
  89. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/x402_config.py +0 -0
  90. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/x402_pricing.py +0 -0
  91. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/mcp/x402_stables.py +0 -0
  92. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/__init__.py +0 -0
  93. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/allocation_analysis.py +0 -0
  94. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/band.py +0 -0
  95. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/breadth.py +0 -0
  96. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/cf_math.py +0 -0
  97. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/cluster.py +0 -0
  98. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/cluster_config.py +0 -0
  99. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/comparison.py +0 -0
  100. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/context.py +0 -0
  101. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/delta.py +0 -0
  102. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/etf.py +0 -0
  103. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/fear_greed.py +0 -0
  104. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/macro.py +0 -0
  105. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/portfolio.py +0 -0
  106. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/portfolio_payload.py +0 -0
  107. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/rebalance.py +0 -0
  108. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/regime.py +0 -0
  109. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/sentiment.py +0 -0
  110. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/snapshots.py +0 -0
  111. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/rollup/tape.py +0 -0
  112. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/status_report.py +0 -0
  113. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/store/__init__.py +0 -0
  114. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/store/db.py +0 -0
  115. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/store/jsonutil.py +0 -0
  116. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/store/meta.py +0 -0
  117. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/store/retention.py +0 -0
  118. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/store/status.py +0 -0
  119. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/timeutil.py +0 -0
  120. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/user_config.py +0 -0
  121. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/x402_production_check.py +0 -0
  122. {alloc_context-0.2.4 → alloc_context-0.2.6}/alloccontext/x402_smoke_redact.py +0 -0
  123. {alloc_context-0.2.4 → alloc_context-0.2.6}/setup.cfg +0 -0
  124. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_backup_sqlite.py +0 -0
  125. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_bridge.py +0 -0
  126. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_bridge_portfolio.py +0 -0
  127. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_bump_version.py +0 -0
  128. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_check_pypi_release_json.py +0 -0
  129. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_coinbase_portfolio.py +0 -0
  130. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_config_cli.py +0 -0
  131. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_context_bundle_schema.py +0 -0
  132. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_context_snapshots.py +0 -0
  133. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_db_schema.py +0 -0
  134. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_dev_stack.py +0 -0
  135. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_etf.py +0 -0
  136. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_exchanges_config.py +0 -0
  137. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_fear_greed.py +0 -0
  138. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_fred.py +0 -0
  139. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_glama_well_known.py +0 -0
  140. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_holdings_scoped_delta_regime.py +0 -0
  141. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_holdings_scoped_market.py +0 -0
  142. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_horizon.py +0 -0
  143. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_ingest_outcome.py +0 -0
  144. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_ingest_runner.py +0 -0
  145. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_ingest_store_integration.py +0 -0
  146. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_kalshi_api.py +0 -0
  147. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_kraken_portfolio.py +0 -0
  148. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_langchain_integration.py +0 -0
  149. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_live_ingest_handlers.py +0 -0
  150. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_macro.py +0 -0
  151. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_market_breadth.py +0 -0
  152. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_assets_regime.py +0 -0
  153. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_bazaar.py +0 -0
  154. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_contracts.py +0 -0
  155. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_data_staleness.py +0 -0
  156. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_handlers.py +0 -0
  157. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_health.py +0 -0
  158. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_http_lifecycle.py +0 -0
  159. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_live_portfolio.py +0 -0
  160. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_server.py +0 -0
  161. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_validation.py +0 -0
  162. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_x402_http.py +0 -0
  163. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_x402_pricing.py +0 -0
  164. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_mcp_x402_stables.py +0 -0
  165. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_payer.py +0 -0
  166. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_portfolio_holdings.py +0 -0
  167. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_quote_resolver.py +0 -0
  168. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_rebalance.py +0 -0
  169. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_rollup.py +0 -0
  170. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_script_runtime.py +0 -0
  171. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_security_hardening.py +0 -0
  172. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_server_json.py +0 -0
  173. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_setup.py +0 -0
  174. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_snapshots_and_delta.py +0 -0
  175. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_status_report.py +0 -0
  176. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_tool_catalog.py +0 -0
  177. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_user_config.py +0 -0
  178. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_workflows.py +0 -0
  179. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_x402_production_check.py +0 -0
  180. {alloc_context-0.2.4 → alloc_context-0.2.6}/tests/test_x402_smoke_redact.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alloc-context
3
- Version: 0.2.4
4
- Summary: Portfolio-aware crypto context for agents holdings-scoped market, sentiment, optional allocation analysis
3
+ Version: 0.2.6
4
+ Summary: Portfolio-aware crypto context CEX or wallet holdings, holdings-scoped market, optional allocation analysis
5
5
  License: Elastic-2.0
6
6
  Project-URL: Homepage, https://mcp.alloc-context.com/llms.txt
7
7
  Project-URL: Documentation, https://github.com/AllocContext/alloc-context/blob/main/docs/agent-integration.md
@@ -9,7 +9,7 @@ Project-URL: Repository, https://github.com/AllocContext/alloc-context
9
9
  Project-URL: Issues, https://github.com/AllocContext/alloc-context/issues
10
10
  Project-URL: Changelog, https://github.com/AllocContext/alloc-context/releases
11
11
  Project-URL: MCP Server, https://mcp.alloc-context.com/mcp
12
- Keywords: mcp,x402,holdings,portfolio,crypto,cryptocurrency,bitcoin,ethereum,agents,allocation,rebalance,coinbase,kraken
12
+ Keywords: mcp,x402,holdings,portfolio,crypto,cryptocurrency,bitcoin,ethereum,agents,allocation,rebalance,coinbase,kraken,wallet,defi
13
13
  Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Programming Language :: Python :: 3
@@ -73,9 +73,10 @@ pip install "alloc-context[mcp,hosted]"
73
73
  **2. User config**
74
74
 
75
75
  Copy [config/user.example.yaml](config/user.example.yaml) to
76
- `~/.config/alloc-context/user.yaml`. Add read-only exchange keys for portfolio
77
- discovery (optional) and an x402 payer for hosted market context. See
78
- [user-config.md](docs/user-config.md).
76
+ `~/.config/alloc-context/user.yaml`. For portfolio discovery (optional): read-only
77
+ CEX keys (e.g. Coinbase, Kraken) in user config, or call hosted
78
+ `get_portfolio_state` with `exchange=wallet` and a public EVM address. Add an x402
79
+ payer for hosted market context. See [user-config.md](docs/user-config.md).
79
80
 
80
81
  **3. MCP config**
81
82
 
@@ -101,9 +102,9 @@ Use an absolute path for `--user-config`. Example:
101
102
 
102
103
  **4. Ask your agent**
103
104
 
104
- Call `get_context_bundle` for a full snapshot (holdings when keys are set,
105
- market/sentiment/macro via hosted upstream). Pure math tools
106
- (`check_allocation_band`, `get_rebalance_plan`) work without exchange keys.
105
+ Call `get_context_bundle` for a full snapshot (holdings when a portfolio source
106
+ is configured, market/sentiment/macro via hosted upstream). Pure math tools
107
+ (`check_allocation_band`, `get_rebalance_plan`) work without portfolio credentials.
107
108
 
108
109
  Full setup guide: [cursor-mcp.md](docs/cursor-mcp.md). Sample responses:
109
110
  [examples.md](docs/examples.md).
@@ -135,7 +136,7 @@ combines local portfolio reads with this upstream for market context.
135
136
  | `get_rebalance_plan` | USD rebalance moves from allocation, target, and NAV |
136
137
  | `check_allocation_band` | Drift vs target and whether allocation is outside the band |
137
138
  | `check_allocation_bands` | Batch band checks for multiple target scenarios |
138
- | `get_portfolio_state` | Live NAV and holdings (e.g. Coinbase, Kraken read-only keys) |
139
+ | `get_portfolio_state` | Live NAV and holdings (CEX keys or public EVM wallet address) |
139
140
 
140
141
  Market context is **holdings-scoped**: band assets (BTC/ETH) use OHLC bars; alt
141
142
  holdings (e.g. HYPE) use quote snapshots when cached. The bridge auto-scopes
@@ -28,9 +28,10 @@ pip install "alloc-context[mcp,hosted]"
28
28
  **2. User config**
29
29
 
30
30
  Copy [config/user.example.yaml](config/user.example.yaml) to
31
- `~/.config/alloc-context/user.yaml`. Add read-only exchange keys for portfolio
32
- discovery (optional) and an x402 payer for hosted market context. See
33
- [user-config.md](docs/user-config.md).
31
+ `~/.config/alloc-context/user.yaml`. For portfolio discovery (optional): read-only
32
+ CEX keys (e.g. Coinbase, Kraken) in user config, or call hosted
33
+ `get_portfolio_state` with `exchange=wallet` and a public EVM address. Add an x402
34
+ payer for hosted market context. See [user-config.md](docs/user-config.md).
34
35
 
35
36
  **3. MCP config**
36
37
 
@@ -56,9 +57,9 @@ Use an absolute path for `--user-config`. Example:
56
57
 
57
58
  **4. Ask your agent**
58
59
 
59
- Call `get_context_bundle` for a full snapshot (holdings when keys are set,
60
- market/sentiment/macro via hosted upstream). Pure math tools
61
- (`check_allocation_band`, `get_rebalance_plan`) work without exchange keys.
60
+ Call `get_context_bundle` for a full snapshot (holdings when a portfolio source
61
+ is configured, market/sentiment/macro via hosted upstream). Pure math tools
62
+ (`check_allocation_band`, `get_rebalance_plan`) work without portfolio credentials.
62
63
 
63
64
  Full setup guide: [cursor-mcp.md](docs/cursor-mcp.md). Sample responses:
64
65
  [examples.md](docs/examples.md).
@@ -90,7 +91,7 @@ combines local portfolio reads with this upstream for market context.
90
91
  | `get_rebalance_plan` | USD rebalance moves from allocation, target, and NAV |
91
92
  | `check_allocation_band` | Drift vs target and whether allocation is outside the band |
92
93
  | `check_allocation_bands` | Batch band checks for multiple target scenarios |
93
- | `get_portfolio_state` | Live NAV and holdings (e.g. Coinbase, Kraken read-only keys) |
94
+ | `get_portfolio_state` | Live NAV and holdings (CEX keys or public EVM wallet address) |
94
95
 
95
96
  Market context is **holdings-scoped**: band assets (BTC/ETH) use OHLC bars; alt
96
97
  holdings (e.g. HYPE) use quote snapshots when cached. The bridge auto-scopes
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alloc-context
3
- Version: 0.2.4
4
- Summary: Portfolio-aware crypto context for agents holdings-scoped market, sentiment, optional allocation analysis
3
+ Version: 0.2.6
4
+ Summary: Portfolio-aware crypto context CEX or wallet holdings, holdings-scoped market, optional allocation analysis
5
5
  License: Elastic-2.0
6
6
  Project-URL: Homepage, https://mcp.alloc-context.com/llms.txt
7
7
  Project-URL: Documentation, https://github.com/AllocContext/alloc-context/blob/main/docs/agent-integration.md
@@ -9,7 +9,7 @@ Project-URL: Repository, https://github.com/AllocContext/alloc-context
9
9
  Project-URL: Issues, https://github.com/AllocContext/alloc-context/issues
10
10
  Project-URL: Changelog, https://github.com/AllocContext/alloc-context/releases
11
11
  Project-URL: MCP Server, https://mcp.alloc-context.com/mcp
12
- Keywords: mcp,x402,holdings,portfolio,crypto,cryptocurrency,bitcoin,ethereum,agents,allocation,rebalance,coinbase,kraken
12
+ Keywords: mcp,x402,holdings,portfolio,crypto,cryptocurrency,bitcoin,ethereum,agents,allocation,rebalance,coinbase,kraken,wallet,defi
13
13
  Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Programming Language :: Python :: 3
@@ -73,9 +73,10 @@ pip install "alloc-context[mcp,hosted]"
73
73
  **2. User config**
74
74
 
75
75
  Copy [config/user.example.yaml](config/user.example.yaml) to
76
- `~/.config/alloc-context/user.yaml`. Add read-only exchange keys for portfolio
77
- discovery (optional) and an x402 payer for hosted market context. See
78
- [user-config.md](docs/user-config.md).
76
+ `~/.config/alloc-context/user.yaml`. For portfolio discovery (optional): read-only
77
+ CEX keys (e.g. Coinbase, Kraken) in user config, or call hosted
78
+ `get_portfolio_state` with `exchange=wallet` and a public EVM address. Add an x402
79
+ payer for hosted market context. See [user-config.md](docs/user-config.md).
79
80
 
80
81
  **3. MCP config**
81
82
 
@@ -101,9 +102,9 @@ Use an absolute path for `--user-config`. Example:
101
102
 
102
103
  **4. Ask your agent**
103
104
 
104
- Call `get_context_bundle` for a full snapshot (holdings when keys are set,
105
- market/sentiment/macro via hosted upstream). Pure math tools
106
- (`check_allocation_band`, `get_rebalance_plan`) work without exchange keys.
105
+ Call `get_context_bundle` for a full snapshot (holdings when a portfolio source
106
+ is configured, market/sentiment/macro via hosted upstream). Pure math tools
107
+ (`check_allocation_band`, `get_rebalance_plan`) work without portfolio credentials.
107
108
 
108
109
  Full setup guide: [cursor-mcp.md](docs/cursor-mcp.md). Sample responses:
109
110
  [examples.md](docs/examples.md).
@@ -135,7 +136,7 @@ combines local portfolio reads with this upstream for market context.
135
136
  | `get_rebalance_plan` | USD rebalance moves from allocation, target, and NAV |
136
137
  | `check_allocation_band` | Drift vs target and whether allocation is outside the band |
137
138
  | `check_allocation_bands` | Batch band checks for multiple target scenarios |
138
- | `get_portfolio_state` | Live NAV and holdings (e.g. Coinbase, Kraken read-only keys) |
139
+ | `get_portfolio_state` | Live NAV and holdings (CEX keys or public EVM wallet address) |
139
140
 
140
141
  Market context is **holdings-scoped**: band assets (BTC/ETH) use OHLC bars; alt
141
142
  holdings (e.g. HYPE) use quote snapshots when cached. The bridge auto-scopes
@@ -56,6 +56,12 @@ alloccontext/ingest/exchange/live.py
56
56
  alloccontext/ingest/exchange/portfolio.py
57
57
  alloccontext/ingest/exchange/registry.py
58
58
  alloccontext/ingest/exchange/types.py
59
+ alloccontext/ingest/wallet/__init__.py
60
+ alloccontext/ingest/wallet/alchemy.py
61
+ alloccontext/ingest/wallet/chains.py
62
+ alloccontext/ingest/wallet/curated_tokens.py
63
+ alloccontext/ingest/wallet/etherscan.py
64
+ alloccontext/ingest/wallet/portfolio.py
59
65
  alloccontext/integrations/__init__.py
60
66
  alloccontext/integrations/langchain.py
61
67
  alloccontext/mcp/__init__.py
@@ -163,6 +169,9 @@ tests/test_snapshots_and_delta.py
163
169
  tests/test_status_report.py
164
170
  tests/test_tool_catalog.py
165
171
  tests/test_user_config.py
172
+ tests/test_wallet_alchemy.py
173
+ tests/test_wallet_etherscan.py
174
+ tests/test_wallet_portfolio.py
166
175
  tests/test_workflows.py
167
176
  tests/test_x402_bazaar_dynamic.py
168
177
  tests/test_x402_production_check.py
@@ -1,3 +1,3 @@
1
1
  """AllocContext — portfolio-aware crypto context for agents (MCP)."""
2
2
 
3
- __version__ = "0.2.4"
3
+ __version__ = "0.2.6"
@@ -8,6 +8,7 @@ from urllib.parse import urlparse
8
8
 
9
9
  import yaml
10
10
 
11
+ from alloccontext.ingest.wallet.chains import DEFAULT_WALLET_CHAIN_IDS
11
12
  from alloccontext.rollup.cluster_config import RollupConfig, load_rollup_config
12
13
 
13
14
 
@@ -101,6 +102,17 @@ class ExchangesConfig:
101
102
  raise ValueError(f"unsupported primary exchange: {self.primary}")
102
103
 
103
104
 
105
+ @dataclass(frozen=True)
106
+ class WalletConfig:
107
+ enabled: bool
108
+ provider: str
109
+ chain_ids: tuple[int, ...]
110
+ min_value_usd: float
111
+ timeout_seconds: float
112
+ max_retries: int
113
+ retry_backoff_seconds: float
114
+
115
+
104
116
  @dataclass(frozen=True)
105
117
  class KalshiSeriesConfig:
106
118
  asset: str
@@ -175,6 +187,7 @@ class AppConfig:
175
187
  ingest: IngestConfig
176
188
  kraken: KrakenConfig
177
189
  exchanges: ExchangesConfig
190
+ wallet: WalletConfig
178
191
  kalshi: KalshiConfig
179
192
  rollup: RollupConfig
180
193
  macro: MacroConfig
@@ -240,6 +253,27 @@ def _kraken_config_from_spot(spot: SpotExchangeConfig) -> KrakenConfig:
240
253
  )
241
254
 
242
255
 
256
+ def _load_wallet_config(raw: dict[str, Any]) -> WalletConfig:
257
+ wallet_raw = raw.get("wallet") or {}
258
+ chain_ids_raw = wallet_raw.get("chain_ids")
259
+ if chain_ids_raw:
260
+ chain_ids = tuple(int(chain_id) for chain_id in chain_ids_raw)
261
+ else:
262
+ chain_ids = DEFAULT_WALLET_CHAIN_IDS
263
+ provider = str(wallet_raw.get("provider") or "alchemy").strip().lower()
264
+ if provider not in {"alchemy", "etherscan"}:
265
+ raise ValueError(f"unsupported wallet.provider: {provider}")
266
+ return WalletConfig(
267
+ enabled=bool(wallet_raw.get("enabled", True)),
268
+ provider=provider,
269
+ chain_ids=chain_ids,
270
+ min_value_usd=float(wallet_raw.get("min_value_usd") or 1.0),
271
+ timeout_seconds=float(wallet_raw.get("timeout_seconds") or 20.0),
272
+ max_retries=int(wallet_raw.get("max_retries") or 3),
273
+ retry_backoff_seconds=float(wallet_raw.get("retry_backoff_seconds") or 2.0),
274
+ )
275
+
276
+
243
277
  def _load_exchanges_config(
244
278
  raw: dict[str, Any],
245
279
  *,
@@ -319,6 +353,7 @@ def load_config(path: str | Path | None = None) -> AppConfig:
319
353
  ingest_sources=ingest_sources,
320
354
  )
321
355
  kraken = _kraken_config_from_spot(exchanges.kraken)
356
+ wallet = _load_wallet_config(raw)
322
357
 
323
358
  return AppConfig(
324
359
  paths=PathsConfig(
@@ -340,6 +375,7 @@ def load_config(path: str | Path | None = None) -> AppConfig:
340
375
  ),
341
376
  kraken=kraken,
342
377
  exchanges=exchanges,
378
+ wallet=wallet,
343
379
  kalshi=KalshiConfig(
344
380
  use_api=bool(kalshi_raw.get("use_api", True)),
345
381
  base_url=_validate_kalshi_base_url(
@@ -13,6 +13,8 @@ COINGECKO_ID_BY_SYMBOL: dict[str, str] = {
13
13
  "BTC": "bitcoin",
14
14
  "ETH": "ethereum",
15
15
  "HYPE": "hyperliquid",
16
+ "POL": "polygon-ecosystem-token",
17
+ "MATIC": "polygon-ecosystem-token",
16
18
  }
17
19
 
18
20
  KRAKEN_ASSET_TO_SYMBOL = {
@@ -15,7 +15,8 @@ from alloccontext.mcp.validation import validate_band, validate_target_pct
15
15
  from alloccontext.rollup.allocation_analysis import build_allocation_analysis
16
16
  from alloccontext.rollup.portfolio_payload import portfolio_dict_from_snapshot
17
17
 
18
- SUPPORTED_EXCHANGES = frozenset({"kraken", "coinbase"})
18
+ SUPPORTED_EXCHANGES = frozenset({"kraken", "coinbase", "wallet"})
19
+ CEX_EXCHANGES = frozenset({"kraken", "coinbase"})
19
20
 
20
21
 
21
22
  class LivePortfolioError(Exception):
@@ -29,6 +30,13 @@ def validate_exchange_id(exchange: str) -> ExchangeId:
29
30
  return key # type: ignore[return-value]
30
31
 
31
32
 
33
+ def validate_cex_exchange_id(exchange: str) -> ExchangeId:
34
+ key = exchange.strip().lower()
35
+ if key not in CEX_EXCHANGES:
36
+ raise ValueError(f"unsupported exchange: {exchange}")
37
+ return key # type: ignore[return-value]
38
+
39
+
32
40
  def _spot_config(config, exchange_id: ExchangeId):
33
41
  if exchange_id == "kraken":
34
42
  return config.exchanges.kraken
@@ -40,7 +48,24 @@ def fetch_live_portfolio_snapshot(
40
48
  api_key: str,
41
49
  api_secret: str,
42
50
  config,
51
+ *,
52
+ wallet_address: str | None = None,
43
53
  ) -> PortfolioSnapshot:
54
+ if exchange_id == "wallet":
55
+ from alloccontext.ingest.wallet.portfolio import (
56
+ WalletPortfolioError,
57
+ fetch_wallet_portfolio_snapshot,
58
+ )
59
+
60
+ if not wallet_address or not wallet_address.strip():
61
+ raise LivePortfolioError("wallet_address is required")
62
+ try:
63
+ return fetch_wallet_portfolio_snapshot(wallet_address, config)
64
+ except WalletPortfolioError as exc:
65
+ raise LivePortfolioError(str(exc)) from exc
66
+ except ValueError as exc:
67
+ raise LivePortfolioError(str(exc)) from exc
68
+
44
69
  spot = _spot_config(config, exchange_id)
45
70
  resolver_config = quote_resolver_config_from_app(config)
46
71
  key = api_key.strip()
@@ -2,4 +2,4 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Literal
4
4
 
5
- ExchangeId = Literal["kraken", "coinbase"]
5
+ ExchangeId = Literal["kraken", "coinbase", "wallet"]
@@ -0,0 +1,6 @@
1
+ from alloccontext.ingest.wallet.portfolio import (
2
+ fetch_wallet_portfolio_snapshot,
3
+ validate_wallet_address,
4
+ )
5
+
6
+ __all__ = ["fetch_wallet_portfolio_snapshot", "validate_wallet_address"]
@@ -0,0 +1,191 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ import requests
8
+
9
+ from alloccontext.ingest.exchange_http import should_retry_exchange_attempt
10
+ from alloccontext.ingest.wallet.chains import alchemy_networks_for_chain_ids
11
+
12
+ ALCHEMY_DATA_BASE = "https://api.g.alchemy.com/data/v1"
13
+
14
+ # Native token metadata is often null; map network slug → band symbol.
15
+ NATIVE_SYMBOL_BY_NETWORK: dict[str, str] = {
16
+ "eth-mainnet": "ETH",
17
+ "arb-mainnet": "ETH",
18
+ "base-mainnet": "ETH",
19
+ "opt-mainnet": "ETH",
20
+ "polygon-mainnet": "POL",
21
+ "matic-mainnet": "POL",
22
+ }
23
+
24
+
25
+ class AlchemyError(Exception):
26
+ pass
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class AlchemyTokenRow:
31
+ symbol: str
32
+ quantity: float
33
+ price_usd: float | None = None
34
+ is_native: bool = False
35
+ token_address: str | None = None
36
+
37
+
38
+ class AlchemyClient:
39
+ def __init__(
40
+ self,
41
+ api_key: str,
42
+ *,
43
+ timeout_seconds: float = 20.0,
44
+ max_retries: int = 3,
45
+ retry_backoff_seconds: float = 2.0,
46
+ ) -> None:
47
+ key = api_key.strip()
48
+ if not key:
49
+ raise AlchemyError("alchemy_api_key_required")
50
+ self._api_key = key
51
+ self._timeout = timeout_seconds
52
+ self._max_retries = max_retries
53
+ self._retry_backoff = retry_backoff_seconds
54
+
55
+ def token_balances(
56
+ self,
57
+ address: str,
58
+ chain_ids: tuple[int, ...],
59
+ ) -> list[AlchemyTokenRow]:
60
+ networks = alchemy_networks_for_chain_ids(chain_ids)
61
+ url = f"{ALCHEMY_DATA_BASE}/{self._api_key}/assets/tokens/by-address"
62
+ rows: list[AlchemyTokenRow] = []
63
+ page_key: str | None = None
64
+ while True:
65
+ payload = self._post(
66
+ url,
67
+ {
68
+ "addresses": [{"address": address, "networks": list(networks)}],
69
+ "includeNativeTokens": True,
70
+ "includeErc20Tokens": True,
71
+ "withMetadata": True,
72
+ "withPrices": True,
73
+ **({"pageKey": page_key} if page_key else {}),
74
+ },
75
+ )
76
+ data = payload.get("data") or {}
77
+ tokens = data.get("tokens") or []
78
+ if not isinstance(tokens, list):
79
+ break
80
+ for token in tokens:
81
+ parsed = _parse_token_row(token)
82
+ if parsed is not None:
83
+ rows.append(parsed)
84
+ page_key = data.get("pageKey")
85
+ if not page_key:
86
+ break
87
+ return rows
88
+
89
+ def _post(self, url: str, body: dict[str, Any]) -> dict[str, Any]:
90
+ last_exc: Exception | None = None
91
+ for attempt in range(self._max_retries + 1):
92
+ try:
93
+ response = requests.post(
94
+ url,
95
+ json=body,
96
+ headers={"Content-Type": "application/json"},
97
+ timeout=self._timeout,
98
+ )
99
+ if response.status_code == 429:
100
+ raise AlchemyError("alchemy_rate_limit")
101
+ response.raise_for_status()
102
+ payload = response.json()
103
+ if not isinstance(payload, dict):
104
+ raise AlchemyError("invalid_alchemy_response")
105
+ if payload.get("error"):
106
+ message = str((payload.get("error") or {}).get("message") or payload)
107
+ raise AlchemyError(message)
108
+ return payload
109
+ except Exception as exc: # noqa: BLE001
110
+ last_exc = exc
111
+ if attempt >= self._max_retries or not _should_retry_alchemy(exc):
112
+ break
113
+ time.sleep(self._retry_backoff * (attempt + 1))
114
+ if isinstance(last_exc, AlchemyError):
115
+ raise last_exc
116
+ if last_exc is not None:
117
+ raise AlchemyError(str(last_exc)) from last_exc
118
+ raise AlchemyError("alchemy_request_failed")
119
+
120
+
121
+ def _should_retry_alchemy(exc: Exception) -> bool:
122
+ if isinstance(exc, AlchemyError):
123
+ detail = str(exc).lower()
124
+ if "rate limit" in detail or "rate_limit" in detail:
125
+ return True
126
+ return should_retry_exchange_attempt(exc)
127
+
128
+
129
+ def _parse_token_row(token: dict[str, Any]) -> AlchemyTokenRow | None:
130
+ if not isinstance(token, dict):
131
+ return None
132
+ network = str(token.get("network") or "")
133
+ token_address = token.get("tokenAddress")
134
+ is_native = not token_address
135
+ metadata = token.get("tokenMetadata") or {}
136
+ symbol = str(metadata.get("symbol") or "").strip()
137
+ if not symbol:
138
+ symbol = NATIVE_SYMBOL_BY_NETWORK.get(network, "")
139
+ if not symbol or len(symbol) > 20:
140
+ return None
141
+ decimals = metadata.get("decimals")
142
+ if is_native:
143
+ decimals_int = 18
144
+ else:
145
+ if decimals is None:
146
+ return None
147
+ try:
148
+ decimals_int = int(decimals)
149
+ except (TypeError, ValueError):
150
+ return None
151
+ quantity = _parse_hex_balance(str(token.get("tokenBalance") or "0x0"), decimals_int)
152
+ if quantity <= 0:
153
+ return None
154
+ price_usd = _price_usd_per_unit(token.get("tokenPrices"))
155
+ contract = str(token_address).lower() if token_address else None
156
+ return AlchemyTokenRow(
157
+ symbol=symbol,
158
+ quantity=quantity,
159
+ price_usd=price_usd,
160
+ is_native=is_native,
161
+ token_address=contract,
162
+ )
163
+
164
+
165
+ def _parse_hex_balance(raw: str, decimals: int) -> float:
166
+ if not raw or raw in {"0x", "0x0"}:
167
+ return 0.0
168
+ try:
169
+ amount = int(raw, 16)
170
+ except ValueError:
171
+ return 0.0
172
+ if amount <= 0:
173
+ return 0.0
174
+ return amount / (10**decimals)
175
+
176
+
177
+ def _price_usd_per_unit(token_prices: Any) -> float | None:
178
+ if not isinstance(token_prices, list):
179
+ return None
180
+ for row in token_prices:
181
+ if not isinstance(row, dict):
182
+ continue
183
+ if str(row.get("currency") or "").lower() != "usd":
184
+ continue
185
+ try:
186
+ value = float(row.get("value"))
187
+ except (TypeError, ValueError):
188
+ continue
189
+ if value > 0:
190
+ return value
191
+ return None
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ # ADR-012 D1: EVM mainnet + major L2s (deterministic, bounded set).
6
+ DEFAULT_WALLET_CHAIN_IDS: tuple[int, ...] = (1, 42161, 8453, 10, 137)
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class EvmChain:
11
+ chain_id: int
12
+ label: str
13
+ native_symbol: str
14
+
15
+
16
+ EVM_CHAINS: dict[int, EvmChain] = {
17
+ 1: EvmChain(chain_id=1, label="ethereum", native_symbol="ETH"),
18
+ 42161: EvmChain(chain_id=42161, label="arbitrum", native_symbol="ETH"),
19
+ 8453: EvmChain(chain_id=8453, label="base", native_symbol="ETH"),
20
+ 10: EvmChain(chain_id=10, label="optimism", native_symbol="ETH"),
21
+ 137: EvmChain(chain_id=137, label="polygon", native_symbol="POL"),
22
+ }
23
+
24
+
25
+ # Alchemy Portfolio API network slugs (see dashboard.alchemy.com/chains).
26
+ ALCHEMY_NETWORK_BY_CHAIN_ID: dict[int, str] = {
27
+ 1: "eth-mainnet",
28
+ 42161: "arb-mainnet",
29
+ 8453: "base-mainnet",
30
+ 10: "opt-mainnet",
31
+ 137: "polygon-mainnet",
32
+ }
33
+
34
+
35
+ def resolve_wallet_chains(chain_ids: tuple[int, ...]) -> tuple[EvmChain, ...]:
36
+ unknown = [chain_id for chain_id in chain_ids if chain_id not in EVM_CHAINS]
37
+ if unknown:
38
+ raise ValueError(f"unsupported wallet chain_ids: {unknown}")
39
+ return tuple(EVM_CHAINS[chain_id] for chain_id in chain_ids)
40
+
41
+
42
+ def alchemy_networks_for_chain_ids(chain_ids: tuple[int, ...]) -> tuple[str, ...]:
43
+ unknown = [chain_id for chain_id in chain_ids if chain_id not in ALCHEMY_NETWORK_BY_CHAIN_ID]
44
+ if unknown:
45
+ raise ValueError(f"unsupported wallet chain_ids for alchemy: {unknown}")
46
+ return tuple(ALCHEMY_NETWORK_BY_CHAIN_ID[chain_id] for chain_id in chain_ids)
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from alloccontext.ingest.wallet.chains import DEFAULT_WALLET_CHAIN_IDS
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class CuratedToken:
10
+ contract: str
11
+ symbol: str
12
+ decimals: int
13
+
14
+
15
+ # Free-tier Etherscan tokenbalance lookups (addresstokenbalance is API Pro).
16
+ _CURATED_BY_CHAIN: dict[int, tuple[CuratedToken, ...]] = {
17
+ 1: (
18
+ CuratedToken("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "USDC", 6),
19
+ CuratedToken("0xdac17f958d2ee523a2206206994597c13d831ec7", "USDT", 6),
20
+ CuratedToken("0x6b175474e89094c44da98b954eedeac495271d0f", "DAI", 18),
21
+ CuratedToken("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "WBTC", 8),
22
+ CuratedToken("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "WETH", 18),
23
+ ),
24
+ 42161: (
25
+ CuratedToken("0xaf88d065e77c8cc2239327c5edb3a432268e5831", "USDC", 6),
26
+ CuratedToken("0xff970a61a04b1ca14834a43f5de4533ebddb5cc8", "USDC", 6),
27
+ CuratedToken("0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9", "USDT", 6),
28
+ CuratedToken("0xda10009cbd5d07dd0cecc66161fc93d7c9000da1", "DAI", 18),
29
+ CuratedToken("0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f", "WBTC", 8),
30
+ CuratedToken("0x82af49447d8a07e3bd95bd0d56f35241523fbab1", "WETH", 18),
31
+ ),
32
+ 8453: (
33
+ CuratedToken("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "USDC", 6),
34
+ CuratedToken("0xd9aaec86b65d6f9524af1fdb8ae83789d97377ce", "USDC", 6),
35
+ CuratedToken("0x4200000000000000000000000000000000000006", "WETH", 18),
36
+ CuratedToken("0x2ae3f1ec7f1f5012cfeab0185bfc2aacee711e6", "CBETH", 18),
37
+ ),
38
+ 10: (
39
+ CuratedToken("0x0b2c639c533813c4aa6d0577d8ec5024ba0262b2", "USDC", 6),
40
+ CuratedToken("0x7f5c764cbc14f9669b88837ca1490cca17c31607", "USDC", 6),
41
+ CuratedToken("0x94b008aa00543c1307b0b1d40c0e6cbfc7971044", "USDT", 6),
42
+ CuratedToken("0xda10009cbd5d07dd0cecc66161fc93d7c9000da1", "DAI", 18),
43
+ CuratedToken("0x68f180fcce6831688e6282403cb1d48dc36a3d3", "WBTC", 8),
44
+ CuratedToken("0x4200000000000000000000000000000000000006", "WETH", 18),
45
+ ),
46
+ 137: (
47
+ CuratedToken("0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "USDC", 6),
48
+ CuratedToken("0x2791bca1f2de4661ed88a30c99a7a9449aa84174", "USDC", 6),
49
+ CuratedToken("0xc2132d05d31c914a87c6611c10748aeb04b58e8f", "USDT", 6),
50
+ CuratedToken("0x8f3cf7ad23cd3cadbd9735aff958023239c6a063", "DAI", 18),
51
+ CuratedToken("0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6", "WBTC", 8),
52
+ CuratedToken("0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "WETH", 18),
53
+ ),
54
+ }
55
+
56
+
57
+ def curated_tokens_for_chain(chain_id: int) -> tuple[CuratedToken, ...]:
58
+ return _CURATED_BY_CHAIN.get(chain_id, ())
59
+
60
+
61
+ def curated_tokens_for_chains(chain_ids: tuple[int, ...]) -> dict[int, tuple[CuratedToken, ...]]:
62
+ return {chain_id: curated_tokens_for_chain(chain_id) for chain_id in chain_ids}
63
+
64
+
65
+ def default_curated_chain_ids() -> tuple[int, ...]:
66
+ return DEFAULT_WALLET_CHAIN_IDS