ncrypted 0.1.0__py3-none-any.whl

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.
ncrypted/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """ncrypted: one-time, end-to-end encrypted secrets for AI agents. https://ncrypted.ai"""
2
+
3
+ from .client import CreatedSecret, Ncrypted, NcryptedError, NcryptedSync, __version__, wallet_path
4
+ from .crypto import MAX_PLAINTEXT_BYTES, decrypt, encrypt, parse_link
5
+
6
+ __all__ = [
7
+ "Ncrypted",
8
+ "NcryptedSync",
9
+ "NcryptedError",
10
+ "CreatedSecret",
11
+ "encrypt",
12
+ "decrypt",
13
+ "parse_link",
14
+ "wallet_path",
15
+ "MAX_PLAINTEXT_BYTES",
16
+ "__version__",
17
+ ]
ncrypted/client.py ADDED
@@ -0,0 +1,249 @@
1
+ """Async client for ncrypted, mirroring the TypeScript SDK (packages/sdk/src/client.ts)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import base64
7
+ import json
8
+ import os
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import httpx
14
+
15
+ from .crypto import decrypt, encrypt, parse_link
16
+
17
+ __version__ = "0.1.0"
18
+ DEFAULT_BASE_URL = "https://ncrypted.ai"
19
+ MAX_PRICE = "$0.01"
20
+ SETTLE_ATTEMPTS = 3
21
+ GONE = "This secret has already been read, burned, or has expired."
22
+ NO_WALLET = (
23
+ "Run `npx ncrypted wallet new` (or create ~/.config/ncrypted/wallet.key) for a payer wallet, send it a little "
24
+ "USDC on Base (0.005 per secret, no ETH needed), or set NCRYPTED_WALLET_PRIVATE_KEY. Requires `pip install ncrypted[pay]`."
25
+ )
26
+
27
+
28
+ class NcryptedError(Exception):
29
+ def __init__(self, message: str, status: int = 0):
30
+ super().__init__(message)
31
+ self.status = status
32
+
33
+
34
+ @dataclass
35
+ class CreatedSecret:
36
+ id: str
37
+ link: str
38
+ """Full one-time link including the decryption key. Treat it like the secret itself."""
39
+ expires_at: str
40
+ receipt_token: str
41
+ emailed: bool
42
+ free: bool
43
+ free_remaining: int | None = None
44
+ payment_tx: str | None = None
45
+
46
+
47
+ def wallet_path() -> Path:
48
+ return Path(os.environ.get("NCRYPTED_WALLET_FILE") or Path.home() / ".config" / "ncrypted" / "wallet.key")
49
+
50
+
51
+ def _normalize_key(raw: str | None) -> str | None:
52
+ v = (raw or "").strip()
53
+ if not v or v.startswith("$"):
54
+ return None
55
+ hexkey = v if v.startswith("0x") else f"0x{v}"
56
+ if len(hexkey) != 66 or any(c not in "0123456789abcdefABCDEF" for c in hexkey[2:]):
57
+ raise ValueError("Wallet private key must be 32 bytes of hex (0x + 64 hex characters)")
58
+ return hexkey
59
+
60
+
61
+ def _wallet_key(explicit: str | None) -> str | None:
62
+ key = _normalize_key(explicit or os.environ.get("NCRYPTED_WALLET_PRIVATE_KEY"))
63
+ if key:
64
+ return key
65
+ try:
66
+ return _normalize_key(wallet_path().read_text())
67
+ except FileNotFoundError:
68
+ return None
69
+
70
+
71
+ def _settlement(res: httpx.Response) -> dict | None:
72
+ header = res.headers.get("payment-response") or res.headers.get("x-payment-response")
73
+ if not header:
74
+ return None
75
+ try:
76
+ return json.loads(base64.b64decode(header))
77
+ except Exception:
78
+ return None
79
+
80
+
81
+ def _error(res: httpx.Response) -> NcryptedError:
82
+ msg = res.reason_phrase
83
+ try:
84
+ msg = res.json().get("error") or msg
85
+ except Exception:
86
+ pass
87
+ if res.status_code == 402:
88
+ reason = (_settlement(res) or {}).get("errorReason")
89
+ msg = f"Payment settlement failed ({reason}). You were not charged." if reason else f"Payment required ({msg})."
90
+ return NcryptedError(msg, res.status_code)
91
+
92
+
93
+ class Ncrypted:
94
+ """Async ncrypted client. The first 3 secrets per day are free; then 0.005 USDC each via x402 (needs ncrypted[pay])."""
95
+
96
+ def __init__(
97
+ self,
98
+ base_url: str | None = None,
99
+ private_key: str | None = None,
100
+ client: str = "python",
101
+ timeout: float = 30.0,
102
+ ):
103
+ self.base_url = (base_url or os.environ.get("NCRYPTED_URL") or DEFAULT_BASE_URL).rstrip("/")
104
+ self._private_key = private_key
105
+ self._headers = {"x-ncrypted-client": f"{client}/{__version__}"}
106
+ self._timeout = timeout
107
+
108
+ def _paying_client(self) -> httpx.AsyncClient | None:
109
+ key = _wallet_key(self._private_key)
110
+ if not key:
111
+ return None
112
+ try:
113
+ from eth_account import Account
114
+ from x402 import x402Client
115
+ from x402.http.clients import x402_httpx_transport
116
+ from x402.mechanisms.evm.exact import register_exact_evm_client
117
+ from x402.mechanisms.evm.signers import EthAccountSigner
118
+ except ImportError as e: # pragma: no cover - depends on extras
119
+ raise NcryptedError(f"Paying for secrets needs the pay extra: pip install 'ncrypted[pay]' ({e})") from e
120
+ x402 = x402Client()
121
+ register_exact_evm_client(x402, EthAccountSigner(Account.from_key(key)))
122
+ x402.set_spend_controls({"max_amount_per_payment": MAX_PRICE})
123
+ return httpx.AsyncClient(transport=x402_httpx_transport(x402), timeout=self._timeout)
124
+
125
+ async def create(
126
+ self,
127
+ secret: str,
128
+ ttl: int = 86400,
129
+ passphrase: str | None = None,
130
+ email_to: str | None = None,
131
+ email_note: str | None = None,
132
+ require_payment: bool = False,
133
+ ) -> CreatedSecret:
134
+ """Encrypt locally and store the ciphertext; returns a one-time link."""
135
+ payer = self._paying_client()
136
+ require_payment = require_payment or bool(email_to)
137
+ if payer is None and require_payment:
138
+ raise NcryptedError(("Email delivery needs a paid secret. " if email_to else "") + NO_WALLET, 402)
139
+ payload, key = encrypt(secret, passphrase)
140
+ headers = {**self._headers, "content-type": "application/json"}
141
+ if require_payment:
142
+ headers["x-ncrypted-no-free"] = "1"
143
+ http = payer or httpx.AsyncClient(timeout=self._timeout)
144
+ async with http:
145
+ for attempt in range(1, SETTLE_ATTEMPTS + 1):
146
+ res = await http.post(f"{self.base_url}/api/v1/secrets", headers=headers, json={**payload, "ttl": ttl})
147
+ settled = _settlement(res)
148
+ if res.status_code == 402 and settled and settled.get("success") is False and attempt < SETTLE_ATTEMPTS:
149
+ continue
150
+ break
151
+ if res.status_code == 402 and payer is None:
152
+ max_ttl = int(res.headers.get("x-ncrypted-free-max-ttl", 86400))
153
+ why = f"Free secrets can last at most {max_ttl // 3600}h." if ttl > max_ttl else "The free secrets for today are used up."
154
+ raise NcryptedError(f"{why} {NO_WALLET}", 402)
155
+ if res.status_code >= 400:
156
+ raise _error(res)
157
+ data = res.json()
158
+ link = f"{data['url']}#{key}"
159
+ emailed = False
160
+ if email_to:
161
+ await self.email(data["id"], data["receiptToken"], link, email_to, email_note)
162
+ emailed = True
163
+ return CreatedSecret(
164
+ id=data["id"],
165
+ link=link,
166
+ expires_at=data["expiresAt"],
167
+ receipt_token=data["receiptToken"],
168
+ emailed=emailed,
169
+ free=bool(data.get("free")),
170
+ free_remaining=data.get("freeRemaining"),
171
+ payment_tx=(_settlement(res) or {}).get("transaction"),
172
+ )
173
+
174
+ async def email(self, id: str, receipt_token: str, link: str, to: str, note: str | None = None) -> None:
175
+ async with httpx.AsyncClient(timeout=self._timeout) as http:
176
+ res = await http.post(
177
+ f"{self.base_url}/api/v1/secrets/{id}/email",
178
+ headers={**self._headers, "authorization": f"Bearer {receipt_token}"},
179
+ json={"to": to, "link": link, "note": note},
180
+ )
181
+ if res.status_code >= 400:
182
+ raise _error(res)
183
+
184
+ async def read(self, link: str, passphrase: str | None = None) -> str:
185
+ """Read and destroy a secret. Free. Works with links from any ncrypted host."""
186
+ p = parse_link(link)
187
+ async with httpx.AsyncClient(timeout=self._timeout) as http:
188
+ res = await http.post(f"{p.origin}/api/v1/secrets/{p.id}/reveal", headers=self._headers)
189
+ if res.status_code in (404, 410):
190
+ raise NcryptedError(GONE, 410)
191
+ if res.status_code >= 400:
192
+ raise _error(res)
193
+ return decrypt(res.json(), p.key, passphrase)
194
+
195
+ async def status(self, id: str, receipt_token: str) -> dict[str, Any]:
196
+ async with httpx.AsyncClient(timeout=self._timeout) as http:
197
+ res = await http.get(
198
+ f"{self.base_url}/api/v1/secrets/{id}/status",
199
+ headers={**self._headers, "authorization": f"Bearer {receipt_token}"},
200
+ )
201
+ if res.status_code >= 400:
202
+ raise _error(res)
203
+ return res.json()
204
+
205
+ async def burn(self, id: str, receipt_token: str) -> None:
206
+ async with httpx.AsyncClient(timeout=self._timeout) as http:
207
+ res = await http.delete(
208
+ f"{self.base_url}/api/v1/secrets/{id}",
209
+ headers={**self._headers, "authorization": f"Bearer {receipt_token}"},
210
+ )
211
+ if res.status_code >= 400:
212
+ raise _error(res)
213
+
214
+ async def quota(self) -> dict[str, Any]:
215
+ async with httpx.AsyncClient(timeout=self._timeout) as http:
216
+ res = await http.get(f"{self.base_url}/api/v1/quota", headers=self._headers)
217
+ if res.status_code >= 400:
218
+ raise _error(res)
219
+ return res.json()
220
+
221
+
222
+ def _run(coro):
223
+ try:
224
+ asyncio.get_running_loop()
225
+ except RuntimeError:
226
+ return asyncio.run(coro)
227
+ raise RuntimeError("Use the async Ncrypted client inside a running event loop")
228
+
229
+
230
+ class NcryptedSync:
231
+ """Blocking wrapper around Ncrypted for scripts and sync frameworks."""
232
+
233
+ def __init__(self, *args: Any, **kwargs: Any):
234
+ self._c = Ncrypted(*args, **kwargs)
235
+
236
+ def create(self, *args: Any, **kwargs: Any) -> CreatedSecret:
237
+ return _run(self._c.create(*args, **kwargs))
238
+
239
+ def read(self, *args: Any, **kwargs: Any) -> str:
240
+ return _run(self._c.read(*args, **kwargs))
241
+
242
+ def status(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
243
+ return _run(self._c.status(*args, **kwargs))
244
+
245
+ def burn(self, *args: Any, **kwargs: Any) -> None:
246
+ return _run(self._c.burn(*args, **kwargs))
247
+
248
+ def quota(self) -> dict[str, Any]:
249
+ return _run(self._c.quota())
ncrypted/crypto.py ADDED
@@ -0,0 +1,90 @@
1
+ """ncrypted v1 envelope, byte-compatible with the TypeScript reference (packages/sdk/src/crypto.ts).
2
+
3
+ AES-256-GCM with a key derived by HKDF-SHA256 from a random 32-byte link key (plus PBKDF2-SHA256 of an optional
4
+ passphrase). The link key travels only in the share link's #fragment, so the service stores ciphertext only.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import os
11
+ import re
12
+ from dataclasses import dataclass
13
+ from urllib.parse import urlsplit
14
+
15
+ from cryptography.exceptions import InvalidTag
16
+ from cryptography.hazmat.primitives import hashes
17
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
18
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
19
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
20
+
21
+ VERSION = 1
22
+ MAX_PLAINTEXT_BYTES = 64 * 1024
23
+ PBKDF2_ITERATIONS = 600_000
24
+ INFO = b"ncrypted v1"
25
+ _LINK_RE = re.compile(r"^/s/([A-Za-z0-9_-]{16,64})/?$")
26
+
27
+
28
+ def to_b64url(data: bytes) -> str:
29
+ return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
30
+
31
+
32
+ def from_b64url(s: str) -> bytes:
33
+ return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
34
+
35
+
36
+ def _derive_key(link_key: bytes, salt: bytes, passphrase: str | None) -> bytes:
37
+ ikm = link_key
38
+ if passphrase:
39
+ stretched = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=PBKDF2_ITERATIONS).derive(
40
+ passphrase.encode("utf-8")
41
+ )
42
+ ikm = link_key + stretched
43
+ return HKDF(algorithm=hashes.SHA256(), length=32, salt=salt, info=INFO).derive(ikm)
44
+
45
+
46
+ def encrypt(plaintext: str, passphrase: str | None = None) -> tuple[dict, str]:
47
+ """Encrypt with a fresh link key. Returns (payload for the server, base64url link key for the #fragment)."""
48
+ data = plaintext.encode("utf-8")
49
+ if not data:
50
+ raise ValueError("Secret is empty")
51
+ if len(data) > MAX_PLAINTEXT_BYTES:
52
+ raise ValueError(f"Secret exceeds {MAX_PLAINTEXT_BYTES} bytes")
53
+ link_key, salt, iv = os.urandom(32), os.urandom(16), os.urandom(12)
54
+ ct = AESGCM(_derive_key(link_key, salt, passphrase)).encrypt(iv, data, None)
55
+ payload = {
56
+ "v": VERSION,
57
+ "ciphertext": to_b64url(ct),
58
+ "iv": to_b64url(iv),
59
+ "salt": to_b64url(salt),
60
+ "hasPassphrase": bool(passphrase),
61
+ }
62
+ return payload, to_b64url(link_key)
63
+
64
+
65
+ def decrypt(payload: dict, link_key: str, passphrase: str | None = None) -> str:
66
+ """Decrypt a payload. Raises ValueError on a wrong key or passphrase, or tampered data."""
67
+ if payload.get("hasPassphrase") and not passphrase:
68
+ raise ValueError("This secret requires a passphrase")
69
+ key = _derive_key(from_b64url(link_key), from_b64url(payload["salt"]), passphrase if payload.get("hasPassphrase") else None)
70
+ try:
71
+ pt = AESGCM(key).decrypt(from_b64url(payload["iv"]), from_b64url(payload["ciphertext"]), None)
72
+ except InvalidTag:
73
+ raise ValueError("Decryption failed: wrong key or passphrase, or the data was tampered with") from None
74
+ return pt.decode("utf-8")
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class ParsedLink:
79
+ origin: str
80
+ id: str
81
+ key: str
82
+
83
+
84
+ def parse_link(link: str) -> ParsedLink:
85
+ """Split https://host/s/{id}#{key}."""
86
+ u = urlsplit(link)
87
+ m = _LINK_RE.match(u.path)
88
+ if not u.scheme or not u.netloc or not m or not u.fragment:
89
+ raise ValueError("Not a valid ncrypted link (expected https://host/s/{id}#{key})")
90
+ return ParsedLink(origin=f"{u.scheme}://{u.netloc}", id=m.group(1), key=u.fragment)
ncrypted/langchain.py ADDED
@@ -0,0 +1,101 @@
1
+ """LangChain tools for ncrypted (pip install 'ncrypted[langchain]', plus 'ncrypted[pay]' to pay beyond the free tier).
2
+
3
+ from ncrypted.langchain import ncrypted_tools
4
+ tools = ncrypted_tools() # pass to create_agent / bind_tools as usual
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Optional
11
+
12
+ from langchain_core.tools import StructuredTool
13
+ from pydantic import BaseModel, Field
14
+
15
+ from .client import Ncrypted, NcryptedError
16
+
17
+
18
+ class _CreateInput(BaseModel):
19
+ secret: str = Field(description="The sensitive value to share (password, API key, token, note). Max 64 KB.")
20
+ ttl_seconds: int = Field(86400, ge=300, le=604800, description="Expiry if never opened. Default 86400 (24h).")
21
+ passphrase: Optional[str] = Field(None, description="Optional second factor; deliver it through a different channel.")
22
+ email_to: Optional[str] = Field(None, description="Email the link to this address (requires a paid secret).")
23
+
24
+
25
+ class _ReadInput(BaseModel):
26
+ link: str = Field(description="The full ncrypted link including the #key fragment.")
27
+ passphrase: Optional[str] = Field(None, description="Required only if the sender set one.")
28
+
29
+
30
+ class _ReceiptInput(BaseModel):
31
+ id: str = Field(description="The secret id returned by ncrypted_create.")
32
+ receipt_token: str = Field(description="The receipt token returned by ncrypted_create.")
33
+
34
+
35
+ def ncrypted_tools(client: Ncrypted | None = None) -> list[StructuredTool]:
36
+ """ncrypted_create, ncrypted_read, ncrypted_status and ncrypted_burn as LangChain tools (sync and async)."""
37
+ nc = client or Ncrypted(client="langchain")
38
+
39
+ async def create(secret: str, ttl_seconds: int = 86400, passphrase: str | None = None, email_to: str | None = None) -> str:
40
+ try:
41
+ r = await nc.create(secret, ttl=ttl_seconds, passphrase=passphrase, email_to=email_to)
42
+ except (NcryptedError, ValueError) as e:
43
+ return f"Error: {e}"
44
+ return json.dumps(
45
+ {
46
+ "link": r.link,
47
+ "id": r.id,
48
+ "expires_at": r.expires_at,
49
+ "receipt_token": r.receipt_token,
50
+ "emailed": r.emailed,
51
+ "free": r.free,
52
+ "note": "The link opens exactly once. Share only the link, never the secret itself.",
53
+ }
54
+ )
55
+
56
+ async def read(link: str, passphrase: str | None = None) -> str:
57
+ try:
58
+ return await nc.read(link, passphrase)
59
+ except (NcryptedError, ValueError) as e:
60
+ return f"Error: {e}"
61
+
62
+ async def status(id: str, receipt_token: str) -> str:
63
+ try:
64
+ return json.dumps(await nc.status(id, receipt_token))
65
+ except NcryptedError as e:
66
+ return f"Error: {e}"
67
+
68
+ async def burn(id: str, receipt_token: str) -> str:
69
+ try:
70
+ await nc.burn(id, receipt_token)
71
+ return json.dumps({"burned": True})
72
+ except NcryptedError as e:
73
+ return f"Error: {e}"
74
+
75
+ def tool(name: str, description: str, coro, schema) -> StructuredTool:
76
+ def sync(**kwargs):
77
+ import asyncio
78
+
79
+ return asyncio.run(coro(**kwargs))
80
+
81
+ return StructuredTool.from_function(func=sync, coroutine=coro, name=name, description=description, args_schema=schema)
82
+
83
+ return [
84
+ tool(
85
+ "ncrypted_create",
86
+ "Encrypt a password, API key, token or private note and return a one-time link that self-destructs after a "
87
+ "single read. Use it whenever a credential must be shared with a person or another agent, instead of writing "
88
+ "the value in a message. Never repeat the secret; share only the link. 3 free per day, then 0.005 USDC each.",
89
+ create,
90
+ _CreateInput,
91
+ ),
92
+ tool(
93
+ "ncrypted_read",
94
+ "Open an ncrypted link (https://ncrypted.ai/s/{id}#{key}) and return the secret. This destroys it permanently. "
95
+ "Free. Use the value only for the task at hand and do not repeat it.",
96
+ read,
97
+ _ReadInput,
98
+ ),
99
+ tool("ncrypted_status", "Check whether a secret you created has been opened (pending, read, burned or expired). Free.", status, _ReceiptInput),
100
+ tool("ncrypted_burn", "Destroy a secret you created before anyone opens it. Free.", burn, _ReceiptInput),
101
+ ]
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.5
2
+ Name: ncrypted
3
+ Version: 0.1.0
4
+ Summary: One-time, end-to-end encrypted secrets for AI agents. 3 free per day, then 0.005 USDC per secret via x402.
5
+ Project-URL: Homepage, https://ncrypted.ai
6
+ Project-URL: Documentation, https://ncrypted.ai/guides
7
+ Project-URL: Source, https://github.com/Seidl-Family/ncrypted
8
+ Project-URL: Issues, https://github.com/Seidl-Family/ncrypted/issues
9
+ Author: Seidl Family
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,crewai,encryption,langchain,mcp,one-time,secrets,x402
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Security :: Cryptography
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: cryptography>=42
18
+ Requires-Dist: httpx>=0.27
19
+ Provides-Extra: all
20
+ Requires-Dist: eth-account>=0.13; extra == 'all'
21
+ Requires-Dist: langchain-core>=0.3; extra == 'all'
22
+ Requires-Dist: x402[evm,httpx]>=2.25; extra == 'all'
23
+ Provides-Extra: dev
24
+ Requires-Dist: eth-account>=0.13; extra == 'dev'
25
+ Requires-Dist: langchain-core>=0.3; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
27
+ Requires-Dist: pytest>=8; extra == 'dev'
28
+ Requires-Dist: x402[evm,httpx]>=2.25; extra == 'dev'
29
+ Provides-Extra: langchain
30
+ Requires-Dist: langchain-core>=0.3; extra == 'langchain'
31
+ Provides-Extra: pay
32
+ Requires-Dist: eth-account>=0.13; extra == 'pay'
33
+ Requires-Dist: x402[evm,httpx]>=2.25; extra == 'pay'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # ncrypted (Python)
37
+
38
+ One-time, end-to-end encrypted secrets for AI agents. Encrypt a password, API key or note locally and get a link that opens exactly once. The first 3 secrets per day are free; after that each costs 0.005 USDC on Base, paid via x402. Reading is always free.
39
+
40
+ ```sh
41
+ pip install ncrypted # free tier, reading
42
+ pip install "ncrypted[pay]" # pay beyond the free tier (x402 on Base)
43
+ pip install "ncrypted[langchain]" # LangChain tools
44
+ ```
45
+
46
+ ```python
47
+ import asyncio
48
+ from ncrypted import Ncrypted
49
+
50
+ async def main():
51
+ nc = Ncrypted()
52
+ s = await nc.create("hunter2", ttl=3600)
53
+ print(s.link) # share this; it opens once
54
+ print(await nc.read(s.link)) # "hunter2", and now it's gone
55
+
56
+ asyncio.run(main())
57
+ ```
58
+
59
+ Blocking code can use `NcryptedSync` with the same methods.
60
+
61
+ ## LangChain
62
+
63
+ ```python
64
+ from ncrypted.langchain import ncrypted_tools
65
+
66
+ tools = ncrypted_tools() # ncrypted_create, ncrypted_read, ncrypted_status, ncrypted_burn
67
+ ```
68
+
69
+ ## Paying for more than the free tier
70
+
71
+ Install `ncrypted[pay]` and provide a dedicated Base wallet holding a little USDC (no ETH needed): `NCRYPTED_WALLET_PRIVATE_KEY`, or the key file at `~/.config/ncrypted/wallet.key` (create it with `npx ncrypted wallet new`). Payments are capped at 0.01 USDC per secret.
72
+
73
+ The wire format is byte-compatible with the TypeScript SDK (`npm install ncrypted`); both decrypt each other's links. Docs: https://ncrypted.ai/guides · Source: https://github.com/Seidl-Family/ncrypted
@@ -0,0 +1,8 @@
1
+ ncrypted/__init__.py,sha256=CR_37zcUb6tCwkvW7aL99VlGhGF_zEMkicLsEymVLBg,466
2
+ ncrypted/client.py,sha256=-B7KOcafu2QPENFFH_cJxPsOQu389vVyRoZE2bhoRE0,9614
3
+ ncrypted/crypto.py,sha256=HtpRmhcxLR_Q0xk3u3HG64kXkS19f0_5k_dQLgY85Ms,3469
4
+ ncrypted/langchain.py,sha256=BkTwZiJPR-tZCDXEPFNxJDgDg6jG517jlAdJXnFXUes,4349
5
+ ncrypted-0.1.0.dist-info/METADATA,sha256=ebgB3asxhgm-zlEmAfxfLROKGmyocS4sPPknlbn1iOk,2956
6
+ ncrypted-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
7
+ ncrypted-0.1.0.dist-info/licenses/LICENSE,sha256=VBVdviLqGa1cYb9KrGBDOmq4T6bu2AhfwLpo14yLX3c,1069
8
+ ncrypted-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Seidl Family
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.