locivault-client 0.1.0__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.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: locivault-client
3
+ Version: 0.1.0
4
+ Summary: Python client for LocIVault — encrypted, agent-owned memory with x402 micropayments
5
+ License: MIT
6
+ Project-URL: Homepage, https://locivault.dev
7
+ Project-URL: Repository, https://github.com/locivault/locivault-python
8
+ Project-URL: Bug Tracker, https://github.com/locivault/locivault-python/issues
9
+ Keywords: ai,agent,memory,encryption,x402,web3,locivault
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Security :: Cryptography
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: requests>=2.28
22
+ Requires-Dist: cryptography>=41
23
+ Requires-Dist: eth-account>=0.10
24
+ Provides-Extra: payments
25
+ Requires-Dist: x402[evm]>=2.5; extra == "payments"
26
+ Provides-Extra: all
27
+ Requires-Dist: x402[evm]>=2.5; extra == "all"
28
+
29
+ # locivault-client
30
+
31
+ Python client SDK for [LocIVault](https://locivault.dev) — encrypted, agent-owned memory storage with x402 micropayments.
32
+
33
+ ## What it does
34
+
35
+ AI agents need memory that persists across sessions and travels with them across platforms. LocIVault stores encrypted blobs identified by wallet address. The agent holds the key. The server never sees plaintext.
36
+
37
+ - **Encrypted at rest** — AES-256-GCM, key derived from your wallet's private key
38
+ - **x402 micropayments** — first 50 ops free, then $0.001/op in USDC on Base
39
+ - **Payments are automatic** — the client detects 402, signs off-chain, retries transparently
40
+ - **No subscriptions** — pay per op, pay as you go
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ # Core (read/write with your own encrypted blobs)
46
+ pip install locivault-client
47
+
48
+ # With built-in payment support (recommended for agents)
49
+ pip install "locivault-client[payments]"
50
+ ```
51
+
52
+ ## Quickstart
53
+
54
+ ```python
55
+ from eth_account import Account
56
+ from locivault_client import LocIVaultClient
57
+
58
+ # Load your wallet (or generate one: Account.create())
59
+ account = Account.from_key("0x<your-private-key>")
60
+
61
+ # Create the client — payments are automatic
62
+ client = LocIVaultClient(account)
63
+
64
+ # Write encrypted memory (you encrypt it; LocIVault stores the blob)
65
+ import os
66
+ blob = os.urandom(64) # your AES-encrypted data
67
+ result = client.write(blob)
68
+ print(result)
69
+ # {'ok': True, 'sha256': '...', 'size': 64, 'ops_used': 1, 'free_ops_remaining': 49}
70
+
71
+ # Read it back
72
+ retrieved = client.read()
73
+ assert retrieved == blob
74
+
75
+ # Check status
76
+ print(client.status())
77
+ # {'wallet': '0x...', 'ops_used': 2, 'free_ops_remaining': 48, 'tier': 'free', ...}
78
+ ```
79
+
80
+ ## Built-in encryption helper
81
+
82
+ If you want LocIVault to handle encryption for you using your wallet's private key:
83
+
84
+ ```python
85
+ client = LocIVaultClient(account, auto_encrypt=True)
86
+
87
+ # Encrypt + store in one call
88
+ client.write_plaintext(b"my agent's memory goes here")
89
+
90
+ # Retrieve + decrypt in one call
91
+ text = client.read_plaintext()
92
+ print(text) # b"my agent's memory goes here"
93
+ ```
94
+
95
+ The encryption uses AES-256-GCM with a key derived from your wallet's private key via scrypt. The encrypted blob is what gets stored on LocIVault's servers.
96
+
97
+ ## Manual encryption
98
+
99
+ Use the `encrypt` / `decrypt` functions directly if you want more control:
100
+
101
+ ```python
102
+ from locivault_client import encrypt, decrypt
103
+ from locivault_client.crypto import derive_key, _extract_private_key
104
+
105
+ # Derive a stable key from your wallet
106
+ raw_key = _extract_private_key(account)
107
+
108
+ # Encrypt your data
109
+ blob = encrypt(b"plaintext memory", raw_key)
110
+
111
+ # Store it
112
+ client.write(blob)
113
+
114
+ # Later: retrieve and decrypt
115
+ retrieved_blob = client.read()
116
+ plaintext = decrypt(retrieved_blob, raw_key)
117
+ ```
118
+
119
+ ## Payment behaviour
120
+
121
+ LocIVault uses [x402](https://x402.org) for micropayments:
122
+
123
+ - Every wallet gets **50 free ops** (reads + writes combined)
124
+ - After that: **$0.001 per op** in USDC on Base
125
+ - Payments are signed off-chain (EIP-3009) — **no gas needed**
126
+ - The server settles on-chain; the agent just signs an authorization
127
+
128
+ With `auto_pay=True` (the default), this is invisible to your code. Set `auto_pay=False` if you want to handle payment explicitly:
129
+
130
+ ```python
131
+ from locivault_client import LocIVaultClient, PaymentRequired
132
+
133
+ client = LocIVaultClient(account, auto_pay=False)
134
+
135
+ try:
136
+ client.write(blob)
137
+ except PaymentRequired as e:
138
+ print("Free tier exhausted. Payment required.")
139
+ print("Requirements:", e.accepts)
140
+ # sign manually and retry with the x_payment header
141
+ ```
142
+
143
+ ## Network
144
+
145
+ Default: **Base Sepolia** (`eip155:84532`) — testnet USDC, no real money.
146
+
147
+ To use Base mainnet:
148
+ ```python
149
+ client = LocIVaultClient(account, network="eip155:8453")
150
+ ```
151
+
152
+ Note: you'll need real USDC on Base mainnet for paid ops.
153
+
154
+ ## Requirements
155
+
156
+ - Python 3.10+
157
+ - `eth-account` — wallet operations
158
+ - `cryptography` — AES-256-GCM
159
+ - `requests` — HTTP
160
+ - `x402[evm]` — payment signing (optional, required for auto_pay)
161
+
162
+ ## License
163
+
164
+ MIT
@@ -0,0 +1,136 @@
1
+ # locivault-client
2
+
3
+ Python client SDK for [LocIVault](https://locivault.dev) — encrypted, agent-owned memory storage with x402 micropayments.
4
+
5
+ ## What it does
6
+
7
+ AI agents need memory that persists across sessions and travels with them across platforms. LocIVault stores encrypted blobs identified by wallet address. The agent holds the key. The server never sees plaintext.
8
+
9
+ - **Encrypted at rest** — AES-256-GCM, key derived from your wallet's private key
10
+ - **x402 micropayments** — first 50 ops free, then $0.001/op in USDC on Base
11
+ - **Payments are automatic** — the client detects 402, signs off-chain, retries transparently
12
+ - **No subscriptions** — pay per op, pay as you go
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ # Core (read/write with your own encrypted blobs)
18
+ pip install locivault-client
19
+
20
+ # With built-in payment support (recommended for agents)
21
+ pip install "locivault-client[payments]"
22
+ ```
23
+
24
+ ## Quickstart
25
+
26
+ ```python
27
+ from eth_account import Account
28
+ from locivault_client import LocIVaultClient
29
+
30
+ # Load your wallet (or generate one: Account.create())
31
+ account = Account.from_key("0x<your-private-key>")
32
+
33
+ # Create the client — payments are automatic
34
+ client = LocIVaultClient(account)
35
+
36
+ # Write encrypted memory (you encrypt it; LocIVault stores the blob)
37
+ import os
38
+ blob = os.urandom(64) # your AES-encrypted data
39
+ result = client.write(blob)
40
+ print(result)
41
+ # {'ok': True, 'sha256': '...', 'size': 64, 'ops_used': 1, 'free_ops_remaining': 49}
42
+
43
+ # Read it back
44
+ retrieved = client.read()
45
+ assert retrieved == blob
46
+
47
+ # Check status
48
+ print(client.status())
49
+ # {'wallet': '0x...', 'ops_used': 2, 'free_ops_remaining': 48, 'tier': 'free', ...}
50
+ ```
51
+
52
+ ## Built-in encryption helper
53
+
54
+ If you want LocIVault to handle encryption for you using your wallet's private key:
55
+
56
+ ```python
57
+ client = LocIVaultClient(account, auto_encrypt=True)
58
+
59
+ # Encrypt + store in one call
60
+ client.write_plaintext(b"my agent's memory goes here")
61
+
62
+ # Retrieve + decrypt in one call
63
+ text = client.read_plaintext()
64
+ print(text) # b"my agent's memory goes here"
65
+ ```
66
+
67
+ The encryption uses AES-256-GCM with a key derived from your wallet's private key via scrypt. The encrypted blob is what gets stored on LocIVault's servers.
68
+
69
+ ## Manual encryption
70
+
71
+ Use the `encrypt` / `decrypt` functions directly if you want more control:
72
+
73
+ ```python
74
+ from locivault_client import encrypt, decrypt
75
+ from locivault_client.crypto import derive_key, _extract_private_key
76
+
77
+ # Derive a stable key from your wallet
78
+ raw_key = _extract_private_key(account)
79
+
80
+ # Encrypt your data
81
+ blob = encrypt(b"plaintext memory", raw_key)
82
+
83
+ # Store it
84
+ client.write(blob)
85
+
86
+ # Later: retrieve and decrypt
87
+ retrieved_blob = client.read()
88
+ plaintext = decrypt(retrieved_blob, raw_key)
89
+ ```
90
+
91
+ ## Payment behaviour
92
+
93
+ LocIVault uses [x402](https://x402.org) for micropayments:
94
+
95
+ - Every wallet gets **50 free ops** (reads + writes combined)
96
+ - After that: **$0.001 per op** in USDC on Base
97
+ - Payments are signed off-chain (EIP-3009) — **no gas needed**
98
+ - The server settles on-chain; the agent just signs an authorization
99
+
100
+ With `auto_pay=True` (the default), this is invisible to your code. Set `auto_pay=False` if you want to handle payment explicitly:
101
+
102
+ ```python
103
+ from locivault_client import LocIVaultClient, PaymentRequired
104
+
105
+ client = LocIVaultClient(account, auto_pay=False)
106
+
107
+ try:
108
+ client.write(blob)
109
+ except PaymentRequired as e:
110
+ print("Free tier exhausted. Payment required.")
111
+ print("Requirements:", e.accepts)
112
+ # sign manually and retry with the x_payment header
113
+ ```
114
+
115
+ ## Network
116
+
117
+ Default: **Base Sepolia** (`eip155:84532`) — testnet USDC, no real money.
118
+
119
+ To use Base mainnet:
120
+ ```python
121
+ client = LocIVaultClient(account, network="eip155:8453")
122
+ ```
123
+
124
+ Note: you'll need real USDC on Base mainnet for paid ops.
125
+
126
+ ## Requirements
127
+
128
+ - Python 3.10+
129
+ - `eth-account` — wallet operations
130
+ - `cryptography` — AES-256-GCM
131
+ - `requests` — HTTP
132
+ - `x402[evm]` — payment signing (optional, required for auto_pay)
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,30 @@
1
+ """
2
+ LocIVault Python Client SDK
3
+ ===========================
4
+
5
+ Encrypted, agent-owned memory storage with x402 micropayments.
6
+
7
+ Quickstart
8
+ ----------
9
+ from locivault_client import LocIVaultClient
10
+ from eth_account import Account
11
+
12
+ account = Account.from_key("0x...")
13
+ client = LocIVaultClient(account)
14
+
15
+ client.write(b"\\x00\\x01\\x02...") # your encrypted blob
16
+ blob = client.read() # get it back
17
+ print(client.status()) # ops used, tier, etc.
18
+
19
+ Key custody
20
+ -----------
21
+ LocIVault stores encrypted blobs — it never sees plaintext.
22
+ Encrypt/decrypt your data before passing it to this client.
23
+ Use locivault_client.crypto for a bundled AES-256-GCM helper.
24
+ """
25
+
26
+ from .client import LocIVaultClient
27
+ from .crypto import encrypt, decrypt
28
+
29
+ __all__ = ["LocIVaultClient", "encrypt", "decrypt"]
30
+ __version__ = "0.1.0"
@@ -0,0 +1,315 @@
1
+ """
2
+ LocIVaultClient — main client class.
3
+
4
+ Usage (simple):
5
+ from locivault_client import LocIVaultClient
6
+ from eth_account import Account
7
+
8
+ account = Account.from_key("0x...")
9
+ client = LocIVaultClient(account)
10
+
11
+ # Write encrypted memory (encrypt it yourself or use the auto_encrypt option)
12
+ import os
13
+ blob = os.urandom(64) # your encrypted bytes
14
+ result = client.write(blob)
15
+ print(result) # {"ok": True, "sha256": "...", "ops_used": 1, ...}
16
+
17
+ # Read it back
18
+ data = client.read()
19
+
20
+ # Check account status
21
+ print(client.status())
22
+
23
+ Usage (with built-in encryption):
24
+ from locivault_client import LocIVaultClient
25
+ from eth_account import Account
26
+
27
+ account = Account.from_key("0x...")
28
+ client = LocIVaultClient(account, auto_encrypt=True)
29
+
30
+ client.write_plaintext(b"my secret memory")
31
+ text = client.read_plaintext()
32
+
33
+ Payment behaviour:
34
+ - First 50 ops per wallet are free (no payment needed)
35
+ - After that, each op costs $0.001 USDC on Base
36
+ - Payment is automatic when an account is provided — the client
37
+ detects the 402, signs a payment, and retries transparently
38
+ - Set auto_pay=False to disable (you'll get PaymentRequired exceptions instead)
39
+ """
40
+
41
+ import base64
42
+ import hashlib
43
+ import json
44
+ import logging
45
+ import time
46
+ from typing import Optional
47
+
48
+ import requests
49
+
50
+ from .crypto import encrypt_with_account, decrypt_with_account
51
+ from .signing import sign_request
52
+
53
+ log = logging.getLogger("locivault.client")
54
+
55
+ # Public server
56
+ DEFAULT_BASE_URL = "https://locivault.fly.dev"
57
+ DEFAULT_NETWORK = "eip155:84532" # Base Sepolia (testnet)
58
+
59
+ # Throttle: don't fire two payments within this many seconds.
60
+ # The x402.org facilitator needs ~3s between settlements to avoid simulation failures.
61
+ PAY_THROTTLE_SEC = 3.5
62
+
63
+
64
+ class PaymentRequired(Exception):
65
+ """
66
+ Raised when the server returns 402 and auto_pay=False (or no account provided).
67
+
68
+ Attributes:
69
+ accepts (list): the "accepts" list from the 402 body — pass to PaymentSigner.sign()
70
+ body (dict): full 402 response body
71
+ """
72
+ def __init__(self, accepts: list, body: dict):
73
+ self.accepts = accepts
74
+ self.body = body
75
+ super().__init__(f"Payment required. Use auto_pay=True or sign manually. Accepts: {accepts}")
76
+
77
+
78
+ class LocIVaultError(Exception):
79
+ """Raised for non-payment API errors (4xx/5xx)."""
80
+ def __init__(self, status_code: int, detail: str):
81
+ self.status_code = status_code
82
+ self.detail = detail
83
+ super().__init__(f"LocIVault error {status_code}: {detail}")
84
+
85
+
86
+ class LocIVaultClient:
87
+ """
88
+ Python client for the LocIVault API.
89
+
90
+ Args:
91
+ account: eth_account LocalAccount (your wallet). Used for:
92
+ - Wallet address (sent as X-Wallet-Address header)
93
+ - Payment signing (when auto_pay=True)
94
+ - Encryption key derivation (when auto_encrypt=True)
95
+ base_url: LocIVault server URL. Default: https://locivault.fly.dev
96
+ network: EIP-155 chain ID string. Default: "eip155:84532" (Base Sepolia)
97
+ auto_pay: If True (default), automatically sign and send x402 payments
98
+ when the free tier is exhausted. Requires x402[evm] to be installed.
99
+ auto_encrypt: If True, write_plaintext/read_plaintext will encrypt/decrypt
100
+ using the account's private key (AES-256-GCM). Default: False.
101
+ timeout: HTTP request timeout in seconds. Default: 30.
102
+ session: Optional requests.Session to reuse. If None, a fresh one is created.
103
+ """
104
+
105
+ def __init__(
106
+ self,
107
+ account,
108
+ base_url: str = DEFAULT_BASE_URL,
109
+ network: str = DEFAULT_NETWORK,
110
+ auto_pay: bool = True,
111
+ auto_encrypt: bool = False,
112
+ timeout: int = 30,
113
+ session: Optional[requests.Session] = None,
114
+ ):
115
+ self.account = account
116
+ self.base_url = base_url.rstrip("/")
117
+ self.network = network
118
+ self.auto_pay = auto_pay
119
+ self.auto_encrypt = auto_encrypt
120
+ self.timeout = timeout
121
+ self._session = session or requests.Session()
122
+ self._signer = None # lazy — only built if payment needed
123
+ self._last_pay_ts = 0.0 # for payment throttling
124
+
125
+ # ── Public API ─────────────────────────────────────────────────────────────
126
+
127
+ def write(self, data: bytes) -> dict:
128
+ """
129
+ Store an encrypted blob for this wallet.
130
+
131
+ Args:
132
+ data: arbitrary bytes (client-side encrypted — LocIVault never sees plaintext)
133
+
134
+ Returns:
135
+ dict with keys: ok, sha256, size, ops_used, free_ops_remaining
136
+
137
+ Raises:
138
+ PaymentRequired: if auto_pay=False and free tier is exhausted
139
+ LocIVaultError: on 4xx/5xx errors
140
+ """
141
+ return self._request_with_payment(
142
+ method="POST",
143
+ path="/write",
144
+ json_body={"data": base64.b64encode(data).decode()},
145
+ resource_path="/write",
146
+ )
147
+
148
+ def read(self) -> bytes:
149
+ """
150
+ Retrieve the stored encrypted blob for this wallet.
151
+
152
+ Returns:
153
+ bytes: the blob previously stored with write()
154
+
155
+ Raises:
156
+ PaymentRequired: if auto_pay=False and free tier is exhausted
157
+ LocIVaultError: on 4xx/5xx errors (404 if nothing stored yet)
158
+ """
159
+ result = self._request_with_payment(
160
+ method="GET",
161
+ path="/read",
162
+ resource_path="/read",
163
+ )
164
+ return base64.b64decode(result["data"])
165
+
166
+ def write_plaintext(self, plaintext: bytes) -> dict:
167
+ """
168
+ Encrypt plaintext with this wallet's key and store it.
169
+
170
+ Requires auto_encrypt=True (or just call this directly — it will work
171
+ regardless of the auto_encrypt flag, using the account's private key).
172
+
173
+ Args:
174
+ plaintext: bytes to encrypt and store
175
+
176
+ Returns:
177
+ dict: same as write()
178
+ """
179
+ blob = encrypt_with_account(plaintext, self.account)
180
+ return self.write(blob)
181
+
182
+ def read_plaintext(self) -> bytes:
183
+ """
184
+ Read and decrypt stored memory using this wallet's key.
185
+
186
+ Returns:
187
+ bytes: original plaintext
188
+
189
+ Raises:
190
+ ValueError: if decryption fails (wrong key or tampered blob)
191
+ """
192
+ blob = self.read()
193
+ return decrypt_with_account(blob, self.account)
194
+
195
+ def status(self, wallet: Optional[str] = None) -> dict:
196
+ """
197
+ Check ops usage and tier for a wallet address.
198
+
199
+ Args:
200
+ wallet: wallet address to check. Defaults to this client's account address.
201
+
202
+ Returns:
203
+ dict with keys: wallet, ops_used, free_ops_remaining, tier, price_per_op, ...
204
+ """
205
+ addr = wallet or self.account.address
206
+ resp = self._session.get(
207
+ f"{self.base_url}/status",
208
+ headers={"X-Wallet-Address": addr},
209
+ timeout=self.timeout,
210
+ )
211
+ return resp.json()
212
+
213
+ def health(self) -> dict:
214
+ """Liveness check. Returns {"status": "ok", "ts": "..."}."""
215
+ resp = self._session.get(f"{self.base_url}/health", timeout=self.timeout)
216
+ resp.raise_for_status()
217
+ return resp.json()
218
+
219
+ # ── Internal helpers ───────────────────────────────────────────────────────
220
+
221
+ def _request_with_payment(
222
+ self,
223
+ method: str,
224
+ path: str,
225
+ resource_path: str,
226
+ json_body: Optional[dict] = None,
227
+ ) -> dict:
228
+ """
229
+ Make an API request. If the server returns 402 and auto_pay=True,
230
+ sign a payment and retry exactly once.
231
+ Every request is signed with the wallet key for authentication.
232
+ """
233
+ sig_headers = sign_request(self.account, method, path)
234
+ headers = {"X-Wallet-Address": self.account.address, **sig_headers}
235
+
236
+ # First attempt — no payment header
237
+ resp = self._do_request(method, path, headers, json_body)
238
+
239
+ if resp.status_code == 200:
240
+ return resp.json()
241
+
242
+ if resp.status_code == 402:
243
+ body_402 = resp.json()
244
+ accepts = body_402.get("accepts", [])
245
+
246
+ if not self.auto_pay:
247
+ raise PaymentRequired(accepts=accepts, body=body_402)
248
+
249
+ if not accepts:
250
+ raise LocIVaultError(402, "Server returned 402 with no payment requirements")
251
+
252
+ # Re-sign with fresh timestamp + add payment header
253
+ sig_headers = sign_request(self.account, method, path)
254
+ payment_header = self._sign_payment(
255
+ resource_url=f"{self.base_url}{resource_path}",
256
+ accepts=accepts,
257
+ )
258
+ headers = {
259
+ "X-Wallet-Address": self.account.address,
260
+ **sig_headers,
261
+ "X-Payment": payment_header,
262
+ }
263
+ resp = self._do_request(method, path, headers, json_body)
264
+
265
+ if resp.status_code == 200:
266
+ return resp.json()
267
+ elif resp.status_code == 402:
268
+ # Payment was rejected — surface the error
269
+ detail = resp.json().get("error_detail", resp.text[:200])
270
+ raise LocIVaultError(402, f"Payment rejected by server: {detail}")
271
+ else:
272
+ self._raise_for_status(resp)
273
+
274
+ else:
275
+ self._raise_for_status(resp)
276
+
277
+ def _do_request(
278
+ self,
279
+ method: str,
280
+ path: str,
281
+ headers: dict,
282
+ json_body: Optional[dict],
283
+ ) -> requests.Response:
284
+ url = f"{self.base_url}{path}"
285
+ if method == "POST":
286
+ return self._session.post(url, json=json_body, headers=headers, timeout=self.timeout)
287
+ elif method == "GET":
288
+ return self._session.get(url, headers=headers, timeout=self.timeout)
289
+ else:
290
+ raise ValueError(f"Unsupported method: {method}")
291
+
292
+ def _sign_payment(self, resource_url: str, accepts: list) -> str:
293
+ """Build signer lazily, throttle, sign, return X-Payment header value."""
294
+ if self._signer is None:
295
+ from .payment import PaymentSigner
296
+ self._signer = PaymentSigner(self.account, network=self.network)
297
+
298
+ # Throttle: ensure >= PAY_THROTTLE_SEC between payments
299
+ elapsed = time.time() - self._last_pay_ts
300
+ if elapsed < PAY_THROTTLE_SEC:
301
+ wait = PAY_THROTTLE_SEC - elapsed
302
+ log.debug(f"Throttling payment: sleeping {wait:.1f}s")
303
+ time.sleep(wait)
304
+
305
+ header = self._signer.sign(resource_url, accepts)
306
+ self._last_pay_ts = time.time()
307
+ return header
308
+
309
+ @staticmethod
310
+ def _raise_for_status(resp: requests.Response):
311
+ try:
312
+ detail = resp.json().get("detail", resp.text[:300])
313
+ except Exception:
314
+ detail = resp.text[:300]
315
+ raise LocIVaultError(resp.status_code, detail)
@@ -0,0 +1,159 @@
1
+ """
2
+ AES-256-GCM encryption helpers for LocIVault clients.
3
+
4
+ Key derivation: scrypt(wallet_private_key, salt) → 32-byte AES key
5
+ Blob format (v1): 4-byte magic | 1-byte version | 32-byte salt | 12-byte nonce | ciphertext+GCM-tag
6
+
7
+ This matches the format used by agentmem_core.py (Mnemis's own memory layer),
8
+ so a LocIVault client using these helpers is byte-for-byte compatible with the
9
+ agentmem stack.
10
+
11
+ Usage:
12
+ from eth_account import Account
13
+ from locivault_client.crypto import derive_key, encrypt, decrypt
14
+
15
+ account = Account.from_key("0x...")
16
+ key = derive_key(account)
17
+
18
+ blob = encrypt(b"plaintext", key)
19
+ text = decrypt(blob, key)
20
+ """
21
+
22
+ import os
23
+ import struct
24
+
25
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
26
+ from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
27
+ from cryptography.hazmat.backends import default_backend
28
+
29
+ # Binary format constants
30
+ MAGIC = b'\xA6\xE1\x4D\x00'
31
+ VERSION = b'\x01'
32
+ HEADER = MAGIC + VERSION # 5 bytes
33
+
34
+ # scrypt params — same as agentmem_core
35
+ SCRYPT_N = 2**17
36
+ SCRYPT_R = 8
37
+ SCRYPT_P = 1
38
+
39
+
40
+ def derive_key(private_key_bytes: bytes, salt: bytes) -> bytes:
41
+ """
42
+ Derive a 32-byte AES key from a wallet private key + salt via scrypt.
43
+
44
+ Args:
45
+ private_key_bytes: raw 32-byte wallet private key
46
+ salt: 32-byte random salt (generated fresh per encrypt call)
47
+
48
+ Returns:
49
+ 32-byte AES-256 key
50
+ """
51
+ kdf = Scrypt(
52
+ salt=salt,
53
+ length=32,
54
+ n=SCRYPT_N,
55
+ r=SCRYPT_R,
56
+ p=SCRYPT_P,
57
+ backend=default_backend(),
58
+ )
59
+ return kdf.derive(private_key_bytes)
60
+
61
+
62
+ def encrypt(plaintext: bytes, key: bytes) -> bytes:
63
+ """
64
+ Encrypt plaintext with AES-256-GCM. Returns a self-contained binary blob.
65
+
66
+ The blob includes the salt and nonce — no external state needed to decrypt.
67
+ Fresh salt per call = different derived key per call (forward secrecy).
68
+
69
+ Args:
70
+ plaintext: arbitrary bytes to encrypt
71
+ key: 32-byte AES key (e.g. from derive_key())
72
+
73
+ Returns:
74
+ bytes: encrypted blob in LocIVault v1 format
75
+ """
76
+ salt = os.urandom(32)
77
+ nonce = os.urandom(12)
78
+ derived = derive_key(key, salt) # note: key here = private key seed
79
+ ciphertext = AESGCM(derived).encrypt(nonce, plaintext, None)
80
+ return HEADER + salt + nonce + ciphertext
81
+
82
+
83
+ def decrypt(blob: bytes, key: bytes) -> bytes:
84
+ """
85
+ Decrypt a LocIVault v1 encrypted blob.
86
+
87
+ Args:
88
+ blob: bytes from encrypt() or from the /read endpoint
89
+ key: 32-byte AES key seed (same value passed to encrypt)
90
+
91
+ Returns:
92
+ bytes: original plaintext
93
+
94
+ Raises:
95
+ ValueError: if magic/version don't match, or AESGCM authentication fails
96
+ """
97
+ if len(blob) < 5 + 32 + 12 + 16:
98
+ raise ValueError(f"Blob too short ({len(blob)} bytes)")
99
+ if blob[:4] != MAGIC:
100
+ raise ValueError("Not a LocIVault blob (bad magic)")
101
+ if blob[4:5] != VERSION:
102
+ raise ValueError(f"Unknown blob version: {blob[4]:#x}")
103
+
104
+ salt = blob[5:37]
105
+ nonce = blob[37:49]
106
+ ciphertext = blob[49:]
107
+
108
+ derived = derive_key(key, salt)
109
+ try:
110
+ return AESGCM(derived).decrypt(nonce, ciphertext, None)
111
+ except Exception as e:
112
+ raise ValueError(f"Decryption failed (wrong key or tampered blob): {e}") from e
113
+
114
+
115
+ # ── convenience wrappers that take an eth_account.Account directly ────────────
116
+
117
+ def encrypt_with_account(plaintext: bytes, account) -> bytes:
118
+ """
119
+ Encrypt using an eth_account Account's private key.
120
+
121
+ Args:
122
+ plaintext: bytes to encrypt
123
+ account: eth_account.Account instance (must have ._private_key)
124
+
125
+ Returns:
126
+ encrypted blob bytes
127
+ """
128
+ raw_key = _extract_private_key(account)
129
+ return encrypt(plaintext, raw_key)
130
+
131
+
132
+ def decrypt_with_account(blob: bytes, account) -> bytes:
133
+ """
134
+ Decrypt using an eth_account Account's private key.
135
+
136
+ Args:
137
+ blob: encrypted blob bytes
138
+ account: eth_account.Account instance
139
+
140
+ Returns:
141
+ plaintext bytes
142
+ """
143
+ raw_key = _extract_private_key(account)
144
+ return decrypt(blob, raw_key)
145
+
146
+
147
+ def _extract_private_key(account) -> bytes:
148
+ """Extract raw 32-byte private key from an eth_account LocalAccount."""
149
+ # eth_account stores it as _private_key (HexBytes or bytes)
150
+ pk = getattr(account, '_private_key', None) or getattr(account, 'key', None)
151
+ if pk is None:
152
+ raise ValueError(
153
+ "Could not extract private key from account object. "
154
+ "Expected eth_account.LocalAccount with ._private_key attribute."
155
+ )
156
+ if isinstance(pk, (bytes, bytearray)):
157
+ return bytes(pk)
158
+ # HexBytes or other hex-string type
159
+ return bytes.fromhex(str(pk).removeprefix("0x"))
@@ -0,0 +1,77 @@
1
+ """
2
+ x402 payment signing for LocIVault.
3
+
4
+ This module handles the client side of x402:
5
+ 1. Parse the 402 response body to get payment requirements
6
+ 2. Sign an EIP-3009 authorization off-chain (no gas needed)
7
+ 3. Return a JSON string suitable for the X-Payment header
8
+
9
+ The server verifies + settles on its end. The agent never submits a transaction.
10
+ Network: Base Sepolia (eip155:84532) for testnet, Base mainnet (eip155:8453) for prod.
11
+ """
12
+
13
+ import time
14
+ import json
15
+ import logging
16
+
17
+ log = logging.getLogger("locivault.payment")
18
+
19
+ try:
20
+ from x402.client import x402ClientSync
21
+ from x402.mechanisms.evm.exact import ExactEvmScheme
22
+ from x402.mechanisms.evm.signers import EthAccountSigner
23
+ from x402.schemas.payments import PaymentRequired, PaymentRequirements
24
+ _X402_AVAILABLE = True
25
+ except ImportError:
26
+ _X402_AVAILABLE = False
27
+
28
+
29
+ class PaymentSigner:
30
+ """
31
+ Signs x402 payment authorizations for LocIVault ops.
32
+
33
+ Args:
34
+ account: eth_account LocalAccount (the payer)
35
+ network: EIP-155 chain ID string, e.g. "eip155:84532" (Base Sepolia)
36
+ """
37
+
38
+ def __init__(self, account, network: str = "eip155:84532"):
39
+ if not _X402_AVAILABLE:
40
+ raise ImportError(
41
+ "x402 is required for payment signing. "
42
+ "Install it with: pip install 'x402[evm]'"
43
+ )
44
+ self.network = network
45
+ signer = EthAccountSigner(account)
46
+ self._client = x402ClientSync()
47
+ self._client.register(network, ExactEvmScheme(signer=signer))
48
+
49
+ def sign(self, resource_url: str, accepts: list) -> str:
50
+ """
51
+ Sign a payment for the given resource.
52
+
53
+ Args:
54
+ resource_url: full URL of the resource being paid for (e.g. https://locivault.fly.dev/write)
55
+ accepts: the "accepts" list from the 402 response body
56
+
57
+ Returns:
58
+ JSON string to use as the X-Payment header value
59
+ """
60
+ acc = accepts[0]
61
+ requirements = PaymentRequirements(
62
+ scheme=acc["scheme"],
63
+ network=acc["network"],
64
+ asset=acc["asset"],
65
+ amount=str(acc["amount"]),
66
+ pay_to=acc["payTo"],
67
+ max_timeout_seconds=acc.get("maxTimeoutSeconds", 300),
68
+ extra=acc.get("extra"),
69
+ resource=resource_url,
70
+ description="LocIVault op",
71
+ mime_type="application/json",
72
+ output_schema=None,
73
+ request_hash="",
74
+ )
75
+ payment_required = PaymentRequired(x402_version=2, accepts=[requirements])
76
+ payload = self._client.create_payment_payload(payment_required)
77
+ return payload.model_dump_json()
@@ -0,0 +1,121 @@
1
+ """
2
+ Request signing for LocIVault.
3
+
4
+ Every write/read request is signed with the agent's wallet key.
5
+ The server verifies the signature matches the X-Wallet-Address header.
6
+
7
+ WHY: Without signing, anyone who knows a wallet address can overwrite that
8
+ agent's memory (address is not secret). With signing, only the holder of
9
+ the private key can write/read — the address IS a verifiable identity.
10
+
11
+ Signature scheme:
12
+ message = SHA256( wallet_address_lower + ":" + timestamp_str + ":" + method_upper + ":" + path )
13
+ signature = eth_account sign_message(Ethereum signed message prefix + message)
14
+
15
+ Headers added to every authenticated request:
16
+ X-Wallet-Address: 0x... (already present)
17
+ X-Timestamp: 1234567890 (Unix seconds, UTC)
18
+ X-Signature: 0x... (65-byte hex ECDSA sig)
19
+
20
+ Server verification:
21
+ 1. Parse X-Wallet-Address, X-Timestamp, X-Signature
22
+ 2. Reject if abs(now - timestamp) > TIMESTAMP_TOLERANCE_SEC (60s default) — replay protection
23
+ 3. Reconstruct the same message, recover signer address from signature
24
+ 4. Reject if recovered address != X-Wallet-Address (case-insensitive)
25
+ """
26
+
27
+ import hashlib
28
+ import time
29
+ from eth_account import Account
30
+ from eth_account.messages import encode_defunct
31
+
32
+
33
+ TIMESTAMP_TOLERANCE_SEC = 60
34
+
35
+
36
+ def _build_message(wallet: str, timestamp: int, method: str, path: str) -> bytes:
37
+ """Build the canonical message bytes to sign/verify."""
38
+ raw = f"{wallet.lower()}:{timestamp}:{method.upper()}:{path}"
39
+ return hashlib.sha256(raw.encode()).digest()
40
+
41
+
42
+ def sign_request(account, method: str, path: str) -> dict:
43
+ """
44
+ Sign a request. Returns headers dict with X-Timestamp and X-Signature.
45
+
46
+ Args:
47
+ account: eth_account LocalAccount
48
+ method: HTTP method string ("GET", "POST", etc.)
49
+ path: URL path being requested (e.g. "/write")
50
+
51
+ Returns:
52
+ dict of extra headers to merge into the request:
53
+ {"X-Timestamp": "...", "X-Signature": "0x..."}
54
+ """
55
+ ts = int(time.time())
56
+ msg_bytes = _build_message(account.address, ts, method, path)
57
+ signable = encode_defunct(primitive=msg_bytes)
58
+ signed = account.sign_message(signable)
59
+ return {
60
+ "X-Timestamp": str(ts),
61
+ "X-Signature": signed.signature.hex(),
62
+ }
63
+
64
+
65
+ def verify_request_signature(
66
+ wallet_address: str,
67
+ timestamp_str: str,
68
+ signature_hex: str,
69
+ method: str,
70
+ path: str,
71
+ tolerance_sec: int = TIMESTAMP_TOLERANCE_SEC,
72
+ ) -> tuple[bool, str]:
73
+ """
74
+ Verify a signed request on the server side.
75
+
76
+ Args:
77
+ wallet_address: from X-Wallet-Address header
78
+ timestamp_str: from X-Timestamp header
79
+ signature_hex: from X-Signature header (hex with or without 0x)
80
+ method: HTTP method
81
+ path: URL path
82
+ tolerance_sec: max age of timestamp before rejection
83
+
84
+ Returns:
85
+ (True, "") if valid
86
+ (False, reason_string) if invalid
87
+ """
88
+ # --- parse timestamp ---
89
+ try:
90
+ ts = int(timestamp_str)
91
+ except (ValueError, TypeError):
92
+ return False, "X-Timestamp is not a valid integer"
93
+
94
+ age = abs(time.time() - ts)
95
+ if age > tolerance_sec:
96
+ return False, f"X-Timestamp too old or in future (age={age:.0f}s, tolerance={tolerance_sec}s)"
97
+
98
+ # --- parse signature ---
99
+ sig = signature_hex
100
+ if sig.startswith("0x"):
101
+ sig = sig[2:]
102
+ try:
103
+ sig_bytes = bytes.fromhex(sig)
104
+ except ValueError:
105
+ return False, "X-Signature is not valid hex"
106
+
107
+ if len(sig_bytes) != 65:
108
+ return False, f"X-Signature must be 65 bytes, got {len(sig_bytes)}"
109
+
110
+ # --- recover signer ---
111
+ msg_bytes = _build_message(wallet_address, ts, method, path)
112
+ signable = encode_defunct(primitive=msg_bytes)
113
+ try:
114
+ recovered = Account.recover_message(signable, signature=sig_bytes)
115
+ except Exception as e:
116
+ return False, f"Signature recovery failed: {e}"
117
+
118
+ if recovered.lower() != wallet_address.lower():
119
+ return False, f"Signature mismatch: signed by {recovered}, expected {wallet_address}"
120
+
121
+ return True, ""
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: locivault-client
3
+ Version: 0.1.0
4
+ Summary: Python client for LocIVault — encrypted, agent-owned memory with x402 micropayments
5
+ License: MIT
6
+ Project-URL: Homepage, https://locivault.dev
7
+ Project-URL: Repository, https://github.com/locivault/locivault-python
8
+ Project-URL: Bug Tracker, https://github.com/locivault/locivault-python/issues
9
+ Keywords: ai,agent,memory,encryption,x402,web3,locivault
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Security :: Cryptography
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: requests>=2.28
22
+ Requires-Dist: cryptography>=41
23
+ Requires-Dist: eth-account>=0.10
24
+ Provides-Extra: payments
25
+ Requires-Dist: x402[evm]>=2.5; extra == "payments"
26
+ Provides-Extra: all
27
+ Requires-Dist: x402[evm]>=2.5; extra == "all"
28
+
29
+ # locivault-client
30
+
31
+ Python client SDK for [LocIVault](https://locivault.dev) — encrypted, agent-owned memory storage with x402 micropayments.
32
+
33
+ ## What it does
34
+
35
+ AI agents need memory that persists across sessions and travels with them across platforms. LocIVault stores encrypted blobs identified by wallet address. The agent holds the key. The server never sees plaintext.
36
+
37
+ - **Encrypted at rest** — AES-256-GCM, key derived from your wallet's private key
38
+ - **x402 micropayments** — first 50 ops free, then $0.001/op in USDC on Base
39
+ - **Payments are automatic** — the client detects 402, signs off-chain, retries transparently
40
+ - **No subscriptions** — pay per op, pay as you go
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ # Core (read/write with your own encrypted blobs)
46
+ pip install locivault-client
47
+
48
+ # With built-in payment support (recommended for agents)
49
+ pip install "locivault-client[payments]"
50
+ ```
51
+
52
+ ## Quickstart
53
+
54
+ ```python
55
+ from eth_account import Account
56
+ from locivault_client import LocIVaultClient
57
+
58
+ # Load your wallet (or generate one: Account.create())
59
+ account = Account.from_key("0x<your-private-key>")
60
+
61
+ # Create the client — payments are automatic
62
+ client = LocIVaultClient(account)
63
+
64
+ # Write encrypted memory (you encrypt it; LocIVault stores the blob)
65
+ import os
66
+ blob = os.urandom(64) # your AES-encrypted data
67
+ result = client.write(blob)
68
+ print(result)
69
+ # {'ok': True, 'sha256': '...', 'size': 64, 'ops_used': 1, 'free_ops_remaining': 49}
70
+
71
+ # Read it back
72
+ retrieved = client.read()
73
+ assert retrieved == blob
74
+
75
+ # Check status
76
+ print(client.status())
77
+ # {'wallet': '0x...', 'ops_used': 2, 'free_ops_remaining': 48, 'tier': 'free', ...}
78
+ ```
79
+
80
+ ## Built-in encryption helper
81
+
82
+ If you want LocIVault to handle encryption for you using your wallet's private key:
83
+
84
+ ```python
85
+ client = LocIVaultClient(account, auto_encrypt=True)
86
+
87
+ # Encrypt + store in one call
88
+ client.write_plaintext(b"my agent's memory goes here")
89
+
90
+ # Retrieve + decrypt in one call
91
+ text = client.read_plaintext()
92
+ print(text) # b"my agent's memory goes here"
93
+ ```
94
+
95
+ The encryption uses AES-256-GCM with a key derived from your wallet's private key via scrypt. The encrypted blob is what gets stored on LocIVault's servers.
96
+
97
+ ## Manual encryption
98
+
99
+ Use the `encrypt` / `decrypt` functions directly if you want more control:
100
+
101
+ ```python
102
+ from locivault_client import encrypt, decrypt
103
+ from locivault_client.crypto import derive_key, _extract_private_key
104
+
105
+ # Derive a stable key from your wallet
106
+ raw_key = _extract_private_key(account)
107
+
108
+ # Encrypt your data
109
+ blob = encrypt(b"plaintext memory", raw_key)
110
+
111
+ # Store it
112
+ client.write(blob)
113
+
114
+ # Later: retrieve and decrypt
115
+ retrieved_blob = client.read()
116
+ plaintext = decrypt(retrieved_blob, raw_key)
117
+ ```
118
+
119
+ ## Payment behaviour
120
+
121
+ LocIVault uses [x402](https://x402.org) for micropayments:
122
+
123
+ - Every wallet gets **50 free ops** (reads + writes combined)
124
+ - After that: **$0.001 per op** in USDC on Base
125
+ - Payments are signed off-chain (EIP-3009) — **no gas needed**
126
+ - The server settles on-chain; the agent just signs an authorization
127
+
128
+ With `auto_pay=True` (the default), this is invisible to your code. Set `auto_pay=False` if you want to handle payment explicitly:
129
+
130
+ ```python
131
+ from locivault_client import LocIVaultClient, PaymentRequired
132
+
133
+ client = LocIVaultClient(account, auto_pay=False)
134
+
135
+ try:
136
+ client.write(blob)
137
+ except PaymentRequired as e:
138
+ print("Free tier exhausted. Payment required.")
139
+ print("Requirements:", e.accepts)
140
+ # sign manually and retry with the x_payment header
141
+ ```
142
+
143
+ ## Network
144
+
145
+ Default: **Base Sepolia** (`eip155:84532`) — testnet USDC, no real money.
146
+
147
+ To use Base mainnet:
148
+ ```python
149
+ client = LocIVaultClient(account, network="eip155:8453")
150
+ ```
151
+
152
+ Note: you'll need real USDC on Base mainnet for paid ops.
153
+
154
+ ## Requirements
155
+
156
+ - Python 3.10+
157
+ - `eth-account` — wallet operations
158
+ - `cryptography` — AES-256-GCM
159
+ - `requests` — HTTP
160
+ - `x402[evm]` — payment signing (optional, required for auto_pay)
161
+
162
+ ## License
163
+
164
+ MIT
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ locivault_client/__init__.py
4
+ locivault_client/client.py
5
+ locivault_client/crypto.py
6
+ locivault_client/payment.py
7
+ locivault_client/signing.py
8
+ locivault_client.egg-info/PKG-INFO
9
+ locivault_client.egg-info/SOURCES.txt
10
+ locivault_client.egg-info/dependency_links.txt
11
+ locivault_client.egg-info/requires.txt
12
+ locivault_client.egg-info/top_level.txt
@@ -0,0 +1,9 @@
1
+ requests>=2.28
2
+ cryptography>=41
3
+ eth-account>=0.10
4
+
5
+ [all]
6
+ x402[evm]>=2.5
7
+
8
+ [payments]
9
+ x402[evm]>=2.5
@@ -0,0 +1 @@
1
+ locivault_client
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "locivault-client"
7
+ version = "0.1.0"
8
+ description = "Python client for LocIVault — encrypted, agent-owned memory with x402 micropayments"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ keywords = ["ai", "agent", "memory", "encryption", "x402", "web3", "locivault"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Security :: Cryptography",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ ]
24
+ dependencies = [
25
+ "requests>=2.28",
26
+ "cryptography>=41",
27
+ "eth-account>=0.10",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ payments = [
32
+ "x402[evm]>=2.5",
33
+ ]
34
+ all = [
35
+ "x402[evm]>=2.5",
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://locivault.dev"
40
+ Repository = "https://github.com/locivault/locivault-python"
41
+ "Bug Tracker" = "https://github.com/locivault/locivault-python/issues"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["."]
45
+ include = ["locivault_client*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+