Persistent storage for AI agents. Store anything, get a cryptographic receipt. pynukez does not move funds — you pay out-of-band and hand us the tx signature to confirm.
pip install pynukezOne command, one install target. Envelope signing works for both Solana-paid (Ed25519) and EVM-paid (secp256k1) lockers out of the box — no extras knob to get wrong.
Requires Python 3.9+.
request_storage()asks the gateway for a quote. You receive payment instructions — address, amount, asset, chain.- You execute the transfer yourself — wallet, CLI, another tool, a hardware signer, whatever. pynukez does not touch your funds.
confirm_storage(pay_req_id, tx_sig=<your_tx_sig>)closes the loop and returns a receipt.- Use the receipt to provision a locker and upload / download / verify files.
The SDK signs API envelopes (Ed25519 for Solana-paid lockers, secp256k1 for EVM-paid lockers) so the gateway can prove requests came from the locker's owner or an authorized operator. That's all the cryptography it does.
from pynukez import Nukez
client = Nukez(keypair_path="~/.config/solana/id.json")
# 1. Ask for payment instructions
request = client.request_storage(units=1)
print(request.next_step)
# -> "Transfer 0.001 SOL to <addr> on solana-devnet,
# then call confirm_storage(pay_req_id='...', tx_sig=<your_tx_signature>)"
# 2. Execute the transfer yourself (wallet, CLI, etc.), capture the tx signature.
tx_sig = "..." # from your wallet / RPC / CLI
# 3. Close the loop with the gateway
receipt = client.confirm_storage(request.pay_req_id, tx_sig=tx_sig)
# 4. Use the receipt
client.provision_locker(receipt.id)
urls = client.create_file(receipt.id, "notes.txt")
client.upload_bytes(urls.upload_url, b"Hello!")
data = client.download_bytes(urls.download_url) # b"Hello!"from pynukez import AsyncNukez
async with AsyncNukez(keypair_path="~/.config/solana/id.json") as client:
request = await client.request_storage(units=1)
# ... execute the transfer externally ...
receipt = await client.confirm_storage(request.pay_req_id, tx_sig=tx_sig)
# ... same methods as sync, just awaited| What you want | Code |
|---|---|
| Buy storage (quote) | request = client.request_storage(units=1) |
| Confirm payment | receipt = client.confirm_storage(request.pay_req_id, tx_sig=<your_tx_sig>) |
| Setup locker | client.provision_locker(receipt.id) |
| Store bytes | urls = client.create_file(receipt.id, "file.txt") then client.upload_bytes(urls.upload_url, data) |
| Store file | client.upload_file_path(receipt.id, "/path/to/file.pdf") |
| Batch upload | client.bulk_upload_paths(receipt.id, [{"filepath": "a.pdf"}, {"filepath": "b.txt"}]) |
| Store directory | client.upload_directory(receipt.id, "/path/to/dir", pattern="*.pdf", recursive=True) |
| Confirm hash | client.confirm_file(receipt.id, "file.txt", confirm_url=urls.confirm_url) |
| Get data | data = client.download_bytes(urls.download_url) |
| List files | files = client.list_files(receipt.id) |
| Delete file | client.delete_file(receipt.id, "file.txt") |
| Verify | result = client.verify_storage(receipt.id) |
| Attest | att = client.attest(receipt.id) |
| Merkle proof | proof = client.get_merkle_proof(receipt.id, "file.txt") |
| Files manifest | client.get_files_manifest(receipt.id) |
| Locker record | client.get_locker_record(receipt.id) |
| Delegate | client.add_operator(receipt.id, operator_pubkey) |
| Viewer link | client.get_owner_viewer_url(receipt.id) |
If your agent runs in a proxied app sandbox (for example, /mnt/data path restrictions), path uploads can fail even when locker auth is valid.
Use the sandbox ingest flow instead:
job = client.sandbox_create_ingest_job(
receipt_id=receipt.id,
files=[{"filename": "image.png", "content_type": "image/png"}],
)
client.sandbox_append_ingest_part(
receipt_id=receipt.id,
job_id=job["job_id"],
file_id=job["files"][0]["file_id"],
part_no=0,
payload_b64="<chunk-0-base64>",
is_last=True,
)
result = client.sandbox_complete_ingest_job(
receipt_id=receipt.id,
job_id=job["job_id"],
)Convenience helpers are available:
client.sandbox_upload_bytes(...)client.sandbox_upload_base64(...)client.sandbox_upload_file_path(...)
Important: if a valid receipt_id already exists, reuse it. Do not purchase storage again unless explicitly requested.
Save your receipt.id — you need it for everything.
# First time
receipt = client.confirm_storage(...)
print(receipt.id) # Save this string somewhere!
# Later — fresh process, reconstructed client:
client.bind_receipt(receipt) # or: bind_receipt(receipt_id=..., owner_identity=...)
files = client.list_files(receipt.id)confirm_storage() primes per-receipt state automatically in the same
process. Across kernel restarts, subprocesses, or receipts loaded from
disk/DB, call bind_receipt(receipt) before owner-only ops
(add_operator, remove_operator) — on dual-key clients, the SDK
refuses to guess which signer to use and raises ReceiptStateNotBoundError
instead.
Change one line:
# Devnet (testing)
client = Nukez(keypair_path="~/.config/solana/id.json", network="devnet")
# Mainnet (production)
client = Nukez(keypair_path="~/.config/solana/id.json", network="mainnet-beta")| Problem | Fix |
|---|---|
| "Transaction not found" | The tx hasn't propagated yet. Wait a few seconds and retry confirm_storage() |
| "URL expired" | Call client.get_file_urls(receipt_id, filename) for fresh URLs |
| "File not found" | Check client.list_files(receipt_id) to see what exists |
ReceiptStateNotBoundError |
Call client.bind_receipt(receipt) before the op (cross-session / fresh-client flows) |
AuthenticationError: Envelope sig_alg '...' incompatible with ... network |
Dual-key client picked wrong signer — call client.bind_receipt(receipt) first |
- Full SDK Reference — Every method, type, and error documented
- Examples — Working code you can copy
- PyPI — Published releases
- GitHub — Source code, issues, releases
- Contributing — Dev setup and PR workflow
MIT