"""Wicked Memory: reference client (Python). One file, copy it into your project. pip install httpx eth-account # required (HTTP + wallet signing) pip install "x402[httpx,evm]" # only if you want search to pay per call automatically Every call is signed with your wallet (EIP-191 personal_sign over method + path + body + timestamp + nonce); the wallet decides WHOSE memories you touch. Search is the only metered call: free with an api_key, otherwise it pays 0.001 USDC per search via x402 from the same wallet (needs USDC on Base). Sync: from wicked_memory import WickedMemorySync mem = WickedMemorySync(private_key=os.environ["AGENT_KEY"], api_key="wm_...") mem.store("The deploy runbook says migrate before start.", tags=["ops"]) print(mem.search("how do we deploy?")["data"][0]["content"]) Async: from wicked_memory import WickedMemory mem = WickedMemory(private_key=..., api_key=...) hits = await mem.search("how do we deploy?") """ from __future__ import annotations import asyncio import hashlib import json import secrets import time from typing import Any from urllib.parse import urlencode import httpx from eth_account import Account from eth_account.messages import encode_defunct DEFAULT_BASE_URL = "https://memory.wickedapi.com" # the host this file was downloaded from class WickedMemoryError(Exception): def __init__(self, status: int, message: str, body: Any = None): super().__init__(f"Wicked Memory {status}: {message}") self.status, self.body = status, body class WickedMemory: def __init__( self, private_key: str, base_url: str = DEFAULT_BASE_URL, api_key: str | None = None, pay: bool = True, network: str | None = None, ): self.account = Account.from_key(private_key) self.base_url = base_url.rstrip("/") self.api_key = api_key self.pay = pay self.network = network or ("eip155:84532" if "testnet" in self.base_url else "eip155:8453") @property def address(self) -> str: """The wallet address your memories are scoped to.""" return self.account.address def _headers(self, method: str, path: str, body: bytes) -> dict[str, str]: timestamp, nonce = str(int(time.time())), secrets.token_hex(16) message = "\n".join( [ "Wicked Memory auth", f"wallet: {self.account.address.lower()}", f"method: {method.upper()}", f"path: {path}", f"timestamp: {timestamp}", f"nonce: {nonce}", f"body-sha256: {hashlib.sha256(body).hexdigest()}", ] ) signature = self.account.sign_message(encode_defunct(text=message)).signature.hex() return { "x-wallet-address": self.account.address, "x-wallet-timestamp": timestamp, "x-wallet-nonce": nonce, "x-wallet-signature": signature if signature.startswith("0x") else "0x" + signature, } def _payment_client(self) -> httpx.AsyncClient: try: from x402 import x402Client from x402.http.clients.httpx import x402HttpxClient from x402.mechanisms.evm import EthAccountSigner from x402.mechanisms.evm.exact import ExactEvmScheme except ImportError as exc: # pragma: no cover raise RuntimeError('Paid search needs `pip install "x402[httpx,evm]"`, or pass api_key, or set pay=False.') from exc client = x402Client() client.register(self.network, ExactEvmScheme(EthAccountSigner(self.account))) return x402HttpxClient(client, timeout=30.0) async def _request( self, method: str, path: str, payload: dict | None = None, extra: dict[str, str] | None = None, paid: bool = False ) -> Any: body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode() if payload is not None else b"" headers = {**self._headers(method, path, body), **(extra or {})} if body: headers["content-type"] = "application/json" http = self._payment_client() if paid else httpx.AsyncClient(timeout=30.0) async with http: resp = await http.request(method, self.base_url + path, content=body or None, headers=headers) try: parsed = resp.json() except ValueError: parsed = None if resp.status_code >= 400: raise WickedMemoryError(resp.status_code, (parsed or {}).get("error", resp.reason_phrase), parsed) return parsed async def store(self, content: str, tags: list[str] | None = None, metadata: dict | None = None, source: str | None = None): """Store a memory (free). A real embedding is computed at write time.""" payload = {"content": content, "tags": tags, "metadata": metadata, "source": source} return await self._request("POST", "/memories", {k: v for k, v in payload.items() if v is not None}) async def search( self, q: str, limit: int | None = None, tags: list[str] | None = None, created_after: str | None = None, created_before: str | None = None, as_of: str | None = None, include_superseded: bool | None = None, ): """Semantic search over YOUR memories. Free with an api_key; otherwise pays 0.001 USDC via x402.""" params: dict[str, Any] = {"q": q} if limit: params["limit"] = limit if tags: params["tags"] = ",".join(tags) for key, value in (("created_after", created_after), ("created_before", created_before), ("as_of", as_of)): if value: params[key] = value if include_superseded is not None: params["include_superseded"] = "true" if include_superseded else "false" path = "/memories/search?" + urlencode(params) if self.api_key: return await self._request("GET", path, extra={"x-api-key": self.api_key}) return await self._request("GET", path, paid=self.pay) async def get(self, memory_id: str): return await self._request("GET", f"/memories/{memory_id}") async def update(self, memory_id: str, content: str | None = None, tags=None, metadata=None, source=None): """Update WITHOUT overwriting: creates a new version and closes the old one. Omitted fields carry over.""" patch = {"content": content, "tags": tags, "metadata": metadata, "source": source} return await self._request("PATCH", f"/memories/{memory_id}", {k: v for k, v in patch.items() if v is not None}) async def history(self, memory_id: str): """Full version chain, oldest first.""" return await self._request("GET", f"/memories/{memory_id}/history") async def delete(self, memory_id: str, scope: str = "chain", reason: str | None = None): """PERMANENT hard delete. scope "chain" (default) removes every version; "version" just this one.""" params = {"scope": scope, **({"reason": reason} if reason else {})} return await self._request("DELETE", f"/memories/{memory_id}?" + urlencode(params)) async def deletions(self): """Your deletion audit log (ids, times, reasons; never content).""" return await self._request("GET", "/memory-deletions") class WickedMemorySync: """Blocking wrapper: same methods as WickedMemory, no asyncio needed (do not call from a running event loop).""" def __init__(self, *args: Any, **kwargs: Any): self._inner = WickedMemory(*args, **kwargs) self.address = self._inner.address def __getattr__(self, name: str): attr = getattr(self._inner, name) if asyncio.iscoroutinefunction(attr): return lambda *a, **k: asyncio.run(attr(*a, **k)) return attr