@agentlayer.tech/wallet 0.1.88 → 0.1.89

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.
@@ -2,7 +2,7 @@
2
2
  "id": "agent-wallet",
3
3
  "name": "Agent Wallet",
4
4
  "description": "Official OpenClaw plugin bridge for the agent-wallet backends, including Solana, local BTC, and local EVM.",
5
- "version": "0.1.88",
5
+ "version": "0.1.89",
6
6
  "contracts": {
7
7
  "tools": [
8
8
  "agentlayer_autonomous_approve",
@@ -61,6 +61,7 @@
61
61
  "manage_evm_morpho_vault_position",
62
62
  "manage_evm_lido_position",
63
63
  "manage_evm_lido_withdrawal",
64
+ "search_uniswap_pairs",
64
65
  "set_evm_network",
65
66
  "set_wallet_backend",
66
67
  "sign_wallet_message",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayertech/agent-wallet-plugin",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
4
4
  "description": "OpenClaw plugin bridge for the AgentLayer wallet runtime.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN ../../../LICENSE",
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.88
1
+ 0.1.89
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Keep in sync with package.json, pyproject.toml, and the npm installer version.
4
4
  # scripts/check_release_version.mjs enforces this on release.
5
- __version__ = "0.1.88"
5
+ __version__ = "0.1.89"
6
6
 
7
7
  __all__ = [
8
8
  "config",
@@ -2211,6 +2211,67 @@ class OpenClawWalletAdapter:
2211
2211
  risk_level="low",
2212
2212
  ),
2213
2213
  )
2214
+ tools.append(
2215
+ AgentToolSpec(
2216
+ name="search_uniswap_pairs",
2217
+ description=(
2218
+ "Search for onchain Uniswap-tradeable token pairs by ticker/name or by exact "
2219
+ "ERC-20 address, with live price, 5m/1h/6h/24h volume, liquidity, FDV, market "
2220
+ "cap, and buy/sell counts. Backed by DexScreener's public index (covers "
2221
+ "Uniswap v2/v3/v4 pools and other DEXes on the same chain), since the Uniswap "
2222
+ "Trading API itself only quotes a pair you already know the addresses for, not "
2223
+ "search-by-name. Defaults to the currently active EVM network (e.g. robinhood); "
2224
+ "set all_chains to search across every chain DexScreener indexes. Read-only "
2225
+ "market data, not a swap quote. Free-text search can surface impersonator "
2226
+ "tokens reusing a legitimate ticker with fabricated liquidity/FDV — before "
2227
+ "quoting or swapping, verify the token_address independently (e.g. via "
2228
+ "get_evm_token_metadata) rather than trusting the top result by symbol match. "
2229
+ "Do not use liquidity/FDV size to pick a winner: observed impersonators on "
2230
+ "robinhood fabricate liquidity/FDV an order of magnitude above the genuine "
2231
+ "pool specifically to look most trustworthy, and a Uniswap quote succeeding "
2232
+ "for a token proves nothing either (Trading API routes any pool with reserves, "
2233
+ "impersonators included). This only matters for tickers claiming to represent "
2234
+ "a real-world asset (stocks/ETFs) - unrelated meme/community tokens on the "
2235
+ "same chain aren't impersonators just for lacking a canonical issuer. When the "
2236
+ "query is a real-world-asset ticker, cross-check the returned token_address "
2237
+ "against Robinhood's own canonical list at docs.robinhood.com/chain/contracts "
2238
+ "(or the equivalent official source for other chains) before relying on the result."
2239
+ ),
2240
+ input_schema={
2241
+ "type": "object",
2242
+ "properties": {
2243
+ "query": {
2244
+ "type": "string",
2245
+ "description": "Free-text search, e.g. a ticker or name (\"AAPL\", \"Apple Robinhood\"). Required unless token_address is given.",
2246
+ },
2247
+ "token_address": {
2248
+ "type": "string",
2249
+ "description": "Exact ERC-20 contract address to look up instead of a free-text query.",
2250
+ },
2251
+ "chain": {
2252
+ "type": "string",
2253
+ "description": "DexScreener chain slug to filter to (e.g. \"robinhood\", \"ethereum\", \"base\"). Defaults to the active EVM network.",
2254
+ },
2255
+ "dex_id": {
2256
+ "type": "string",
2257
+ "description": "Optional exact DEX filter, e.g. \"uniswap\".",
2258
+ },
2259
+ "all_chains": {
2260
+ "type": "boolean",
2261
+ "description": "If true, do not filter query results to a single chain. Ignored when token_address is set.",
2262
+ },
2263
+ "limit": {
2264
+ "type": "integer",
2265
+ "description": "Max number of pairs to return (1-30). Defaults to 10.",
2266
+ },
2267
+ },
2268
+ "required": [],
2269
+ "additionalProperties": False,
2270
+ },
2271
+ read_only=True,
2272
+ risk_level="low",
2273
+ ),
2274
+ )
2214
2275
  tools.insert(
2215
2276
  12,
2216
2277
  AgentToolSpec(
@@ -5460,6 +5521,37 @@ class OpenClawWalletAdapter:
5460
5521
  )
5461
5522
  return AgentToolResult(tool=tool_name, ok=True, data=data)
5462
5523
 
5524
+ if tool_name == "search_uniswap_pairs":
5525
+ query = args.get("query")
5526
+ token_address = args.get("token_address")
5527
+ chain = args.get("chain")
5528
+ dex_id = args.get("dex_id")
5529
+ all_chains = bool(args.get("all_chains", False))
5530
+ limit = args.get("limit", 10)
5531
+ if query is not None and not isinstance(query, str):
5532
+ raise WalletBackendError("query must be a string.")
5533
+ if token_address is not None and not isinstance(token_address, str):
5534
+ raise WalletBackendError("token_address must be a string.")
5535
+ if not (isinstance(query, str) and query.strip()) and not (
5536
+ isinstance(token_address, str) and token_address.strip()
5537
+ ):
5538
+ raise WalletBackendError("Either query or token_address is required.")
5539
+ if chain is not None and not isinstance(chain, str):
5540
+ raise WalletBackendError("chain must be a string.")
5541
+ if dex_id is not None and not isinstance(dex_id, str):
5542
+ raise WalletBackendError("dex_id must be a string.")
5543
+ if not isinstance(limit, int) or limit < 1 or limit > 30:
5544
+ raise WalletBackendError("limit must be an integer between 1 and 30.")
5545
+ data = await active_backend.search_uniswap_pairs(
5546
+ query=query,
5547
+ token_address=token_address,
5548
+ chain=chain,
5549
+ dex_id=dex_id,
5550
+ all_chains=all_chains,
5551
+ limit=limit,
5552
+ )
5553
+ return AgentToolResult(tool=tool_name, ok=True, data=data)
5554
+
5463
5555
  if tool_name == "swap_evm_uniswap_tokens":
5464
5556
  token_in = args.get("token_in")
5465
5557
  token_out = args.get("token_out")
@@ -0,0 +1,145 @@
1
+ """DexScreener provider for onchain token/pair discovery (price, volume, liquidity).
2
+
3
+ Public API, no key required: https://docs.dexscreener.com/api/reference. Used to
4
+ answer "which pairs exist for this token/ticker" the way app.uniswap.org's own
5
+ search box does, since the Uniswap Trading API itself only exposes quote/order/swap
6
+ for a pair you already know, not a search-by-name/address endpoint.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+ from typing import Any
13
+
14
+ from agent_wallet.exceptions import ProviderError
15
+ from agent_wallet.http_client import get_client
16
+
17
+ DEXSCREENER_BASE_URL = "https://api.dexscreener.com"
18
+
19
+ # DexScreener's own responses are served with `cache-control: public, max-age=30`;
20
+ # mirroring that TTL avoids redundant lookups within a session without serving
21
+ # data staler than DexScreener itself would.
22
+ _CACHE_TTL_SECONDS = 30
23
+ _CACHE_MAX_ENTRIES = 128
24
+ _cache: dict[str, tuple[float, list[dict[str, Any]]]] = {}
25
+
26
+
27
+ def _cache_get(key: str) -> list[dict[str, Any]] | None:
28
+ entry = _cache.get(key)
29
+ if entry is None:
30
+ return None
31
+ stored_at, pairs = entry
32
+ if time.monotonic() - stored_at > _CACHE_TTL_SECONDS:
33
+ _cache.pop(key, None)
34
+ return None
35
+ return pairs
36
+
37
+
38
+ def _cache_set(key: str, pairs: list[dict[str, Any]]) -> None:
39
+ if len(_cache) >= _CACHE_MAX_ENTRIES and key not in _cache:
40
+ oldest_key = min(_cache, key=lambda existing: _cache[existing][0])
41
+ _cache.pop(oldest_key, None)
42
+ _cache[key] = (time.monotonic(), pairs)
43
+
44
+
45
+ async def _get(path: str, *, params: dict[str, Any] | None = None) -> Any:
46
+ client = get_client()
47
+ response = await client.get(f"{DEXSCREENER_BASE_URL}{path}", params=params)
48
+ if response.status_code != 200:
49
+ raise ProviderError("dexscreener", f"HTTP {response.status_code}: {response.text[:300]}")
50
+ try:
51
+ return response.json()
52
+ except ValueError as exc:
53
+ raise ProviderError("dexscreener", "Unexpected non-JSON response from DexScreener.") from exc
54
+
55
+
56
+ def _extract_pairs(data: Any) -> list[dict[str, Any]]:
57
+ if isinstance(data, dict):
58
+ pairs = data.get("pairs")
59
+ return pairs if isinstance(pairs, list) else []
60
+ if isinstance(data, list):
61
+ return [item for item in data if isinstance(item, dict)]
62
+ return []
63
+
64
+
65
+ async def search_pairs(query: str) -> list[dict[str, Any]]:
66
+ """Free-text search across every chain/DEX DexScreener indexes."""
67
+ query = query.strip()
68
+ if not query:
69
+ raise ProviderError("dexscreener", "query must not be empty.")
70
+ cache_key = f"search:{query.lower()}"
71
+ cached = _cache_get(cache_key)
72
+ if cached is not None:
73
+ return cached
74
+ data = await _get("/latest/dex/search", params={"q": query})
75
+ pairs = _extract_pairs(data)
76
+ _cache_set(cache_key, pairs)
77
+ return pairs
78
+
79
+
80
+ async def get_pairs_for_token(*, chain: str, token_address: str) -> list[dict[str, Any]]:
81
+ """All pairs for a single token address on one chain, across every DEX on it."""
82
+ chain = chain.strip().lower()
83
+ token_address = token_address.strip()
84
+ if not chain:
85
+ raise ProviderError("dexscreener", "chain must not be empty.")
86
+ if not token_address:
87
+ raise ProviderError("dexscreener", "token_address must not be empty.")
88
+ cache_key = f"token:{chain}:{token_address.lower()}"
89
+ cached = _cache_get(cache_key)
90
+ if cached is not None:
91
+ return cached
92
+ data = await _get(f"/tokens/v1/{chain}/{token_address}")
93
+ pairs = _extract_pairs(data)
94
+ _cache_set(cache_key, pairs)
95
+ return pairs
96
+
97
+
98
+ def normalize_pair(pair: dict[str, Any]) -> dict[str, Any]:
99
+ """Flatten a DexScreener pair payload into the shape handed back to the agent."""
100
+ base_token = pair.get("baseToken") or {}
101
+ quote_token = pair.get("quoteToken") or {}
102
+ volume = pair.get("volume") or {}
103
+ liquidity = pair.get("liquidity") or {}
104
+ price_change = pair.get("priceChange") or {}
105
+ txns = pair.get("txns") or {}
106
+ return {
107
+ "chain_id": pair.get("chainId"),
108
+ "dex_id": pair.get("dexId"),
109
+ "pair_address": pair.get("pairAddress"),
110
+ "url": pair.get("url"),
111
+ "labels": pair.get("labels") or [],
112
+ "base_token": {
113
+ "address": base_token.get("address"),
114
+ "name": base_token.get("name"),
115
+ "symbol": base_token.get("symbol"),
116
+ },
117
+ "quote_token": {
118
+ "address": quote_token.get("address"),
119
+ "name": quote_token.get("name"),
120
+ "symbol": quote_token.get("symbol"),
121
+ },
122
+ "price_usd": pair.get("priceUsd"),
123
+ "price_native": pair.get("priceNative"),
124
+ "price_change_pct": {
125
+ "m5": price_change.get("m5"),
126
+ "h1": price_change.get("h1"),
127
+ "h6": price_change.get("h6"),
128
+ "h24": price_change.get("h24"),
129
+ },
130
+ "volume_usd": {
131
+ "m5": volume.get("m5"),
132
+ "h1": volume.get("h1"),
133
+ "h6": volume.get("h6"),
134
+ "h24": volume.get("h24"),
135
+ },
136
+ "txns": {
137
+ window: {"buys": counts.get("buys"), "sells": counts.get("sells")}
138
+ for window, counts in txns.items()
139
+ if isinstance(counts, dict)
140
+ },
141
+ "liquidity_usd": liquidity.get("usd"),
142
+ "fdv_usd": pair.get("fdv"),
143
+ "market_cap_usd": pair.get("marketCap"),
144
+ "pair_created_at": pair.get("pairCreatedAt"),
145
+ }
@@ -325,6 +325,18 @@ class AgentWalletBackend(ABC):
325
325
  ) -> dict[str, Any]:
326
326
  raise WalletBackendError(f"{self.name} does not support Uniswap swap previews.")
327
327
 
328
+ async def search_uniswap_pairs(
329
+ self,
330
+ *,
331
+ query: str | None = None,
332
+ token_address: str | None = None,
333
+ chain: str | None = None,
334
+ dex_id: str | None = None,
335
+ all_chains: bool = False,
336
+ limit: int = 10,
337
+ ) -> dict[str, Any]:
338
+ raise WalletBackendError(f"{self.name} does not support Uniswap pair search.")
339
+
328
340
  async def send_uniswap_swap(
329
341
  self,
330
342
  *,
@@ -7,7 +7,7 @@ from typing import Any
7
7
 
8
8
  from agent_wallet.config import normalize_evm_network
9
9
  from agent_wallet.providers.evm_portfolio import build_portfolio_snapshot
10
- from agent_wallet.providers import lifi
10
+ from agent_wallet.providers import dexscreener, lifi
11
11
  from agent_wallet.providers.wdk_evm_local import WdkEvmLocalClient
12
12
  from agent_wallet.wallet_layer.base import AgentWalletBackend, WalletBackendError, WalletCapabilities
13
13
 
@@ -1920,6 +1920,43 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
1920
1920
  "swap_transaction": swap_transaction,
1921
1921
  }
1922
1922
 
1923
+ async def search_uniswap_pairs(
1924
+ self,
1925
+ *,
1926
+ query: str | None = None,
1927
+ token_address: str | None = None,
1928
+ chain: str | None = None,
1929
+ dex_id: str | None = None,
1930
+ all_chains: bool = False,
1931
+ limit: int = 10,
1932
+ ) -> dict[str, Any]:
1933
+ query = query.strip() if isinstance(query, str) else None
1934
+ token_address = token_address.strip() if isinstance(token_address, str) else None
1935
+ if not query and not token_address:
1936
+ raise WalletBackendError("Either query or token_address is required.")
1937
+ resolved_chain = (chain or self.network).strip().lower()
1938
+ if token_address:
1939
+ pairs = await dexscreener.get_pairs_for_token(chain=resolved_chain, token_address=token_address)
1940
+ else:
1941
+ pairs = await dexscreener.search_pairs(query)
1942
+ if not all_chains:
1943
+ pairs = [p for p in pairs if str(p.get("chainId") or "").lower() == resolved_chain]
1944
+ if dex_id:
1945
+ wanted_dex = dex_id.strip().lower()
1946
+ pairs = [p for p in pairs if str(p.get("dexId") or "").lower() == wanted_dex]
1947
+ bounded_limit = max(1, min(int(limit or 10), 30))
1948
+ normalized = [dexscreener.normalize_pair(pair) for pair in pairs[:bounded_limit]]
1949
+ return {
1950
+ "chain": None if all_chains else resolved_chain,
1951
+ "all_chains": all_chains,
1952
+ "query": query,
1953
+ "token_address": token_address,
1954
+ "dex_id": dex_id,
1955
+ "count": len(normalized),
1956
+ "pairs": normalized,
1957
+ "source": "dexscreener",
1958
+ }
1959
+
1923
1960
  async def send_uniswap_swap(
1924
1961
  self,
1925
1962
  *,
@@ -2,7 +2,7 @@
2
2
  "id": "agent-wallet",
3
3
  "name": "Agent Wallet",
4
4
  "description": "Plugin-friendly wallet backend for OpenClaw agents with safe wallet tools and runtime instructions across Solana, local BTC, and local EVM.",
5
- "version": "0.1.88",
5
+ "version": "0.1.89",
6
6
  "skills": ["skills/wallet-operator"],
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.88"
7
+ version = "0.1.89"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
3
  "displayName": "Agent Wallet",
4
- "version": "0.1.88",
4
+ "version": "0.1.89",
5
5
  "description": "Claude Code bridge for the existing AgentLayer wallet runtime. Connects to Solana, Bitcoin, and EVM wallets without creating a new one.",
6
6
  "author": {
7
7
  "name": "AgentLayer"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
4
4
  "description": "Codex plugin bridge for the AgentLayer wallet runtime.",
5
5
  "author": {
6
6
  "name": "AgentLayer"
@@ -1,5 +1,5 @@
1
1
  name: agent-wallet
2
- version: 0.1.88
2
+ version: 0.1.89
3
3
  description: Thin Hermes Agent bridge to the existing AgentLayer/OpenClaw wallet backend
4
4
  provides_tools:
5
5
  - agent_wallet_tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-btc-wallet",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate BTC-only wallet service built on Tether WDK.",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-evm-wallet",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate EVM wallet service built on Tether WDK.",