Welcome to the Aleo Python SDK! This SDK provides a set of libraries aimed at empowering Python developers with zk (zero-knowledge) capabilities.
The SDK ships two layers:
- A Web3.py-style facade (
aleo.Aleo/aleo.AsyncAleo) — a high-level, batteries-included client for connecting to a node, managing accounts, reading state, and building/proving/broadcasting transactions. - Low-level primitives (
aleo.mainnet,aleo.testnet) — direct Python bindings to Aleo's zero-knowledge cryptographic types, for when you need full control.
Built with snarkvm 4.8.1 (MainnetV0). For build instructions, see sdk/Readme.md.
The shield-swap-sdk package ships everything an AI
agent needs to drive the shield_swap AMM — set up an account, redeem an
invite code, get the airdrop, make private swaps, manage liquidity, and
collect earnings — from a single generated guide.
Any agent (Claude Code, Codex, Cursor, custom):
pip install shield-swap-sdk
python -m aleo_shield_swap > AGENTS.md # drop the agent guide into your projectMost coding agents (Codex, Cursor, Claude Code, …) automatically read a
repo-root AGENTS.md, so after that one command just chat: "set up a
shield-swap account and get tokens", "find pools and start swapping".
(Equivalently, open with "run python -m aleo_shield_swap and follow that
guide", or paste the output into the agent's instructions.) The one thing
the agent will ask you for is an invite code; everything else — key
material, API credentials, airdrop — is handled by the SDK. Bring an existing account by exporting
SHIELD_SWAP_PRIVATE_KEY (or SHIELD_SWAP_PRIVATE_KEY_FILE) before the
first run — never paste a private key into the chat.
Claude Code: working in this repo, the skill is already installed
(.claude/skills/shield-swap/) — just ask. In your own project, copy the
packaged skill in:
cp -r "$(python -c 'import aleo_shield_swap, pathlib; print(pathlib.Path(aleo_shield_swap.__file__).parent / "skills")')" .claude/skills/shield-swapMCP clients (Claude Desktop, etc.):
pip install "shield-swap-sdk[mcp]"
python -m aleo_shield_swap.mcp # stdio server with the lifecycle toolsfrom aleo import Aleo
# Connect (construction is offline — no I/O until you make a call)
aleo = Aleo(Aleo.HTTPProvider("https://api.provable.com/v2"))
print(aleo.network_name) # "mainnet"
print(aleo.network_id) # 0
# Create an account (local)
account = aleo.account.create()
print(account.address) # aleo1…
# Read a public balance # requires a live node
balance = aleo.get_balance(str(account.address))
print(aleo.from_microcredits(balance), "credits")Aleo.HTTPProvider is also importable directly as from aleo import HTTPProvider; the two are equivalent.
The facade follows a clean top-to-bottom narrative: connect → account → read → build a call → inspect → send. Each rung does a little more than the one above it.
from aleo import Aleo
aleo = Aleo(Aleo.HTTPProvider("https://api.provable.com/v2"))
# Optional: check reachability # requires a live node
if aleo.is_connected():
print("connected to", aleo.network_name)HTTPProvider is a config object, not a live connection — it is safe to construct without a network. It accepts network= ("mainnet" / "testnet"), api_key=, prover_uri= (the DPS endpoint), headers=, and a custom transport= callable.
# Fresh random account
account = aleo.account.create()
# Import from a private-key string (or a PrivateKey object)
account = aleo.account.from_private_key("APrivateKey1zkp…")
# Derive deterministically from a field seed
account = aleo.account.from_seed("123field")
# Sign / verify (bytes)
sig = aleo.account.sign(b"hello aleo", account)
assert aleo.account.verify(str(account.address), b"hello aleo", sig)
# Sign / verify a typed Aleo value
sv = aleo.account.sign_value("100u64", account)
assert aleo.account.verify_value(str(account.address), "100u64", sv)Set aleo.default_account = account and the verbs below will use it whenever you omit the signer.
Note: the facade deliberately has no "recover signer from signature" verb. Aleo is a privacy chain — surfacing "which address signed this?" is a de-anonymisation vector. The low-level
Signature.to_address()primitive remains available directly if you truly need it.
# Public credits balance in microcredits (0 if the address is unfunded) # requires a live node
micro = aleo.get_balance(str(account.address))
print(aleo.from_microcredits(micro), "credits")
# Read any on-chain mapping through a bound Program # requires a live node
credits = aleo.programs.get("credits.aleo")
raw = credits.mapping("account").get(str(account.address))
print("account mapping value:", raw)Unit helpers are local: aleo.to_microcredits(1.5) == 1_500_000 and aleo.from_microcredits(1_500_000) == 1.5. Address validation is local too: aleo.is_valid_address(s) -> bool.
aleo.programs.get(...) fetches a program and exposes its transitions as program.functions.<name>, mirroring web3.py's ABI-driven contract.functions. Calling one coerces your Python arguments to Aleo values and returns a BoundCall:
# requires a live node (to fetch the program source)
credits = aleo.programs.get("credits.aleo")
call = credits.functions.transfer_public(str(account.address), 1_000_000)
print(call.signature) # "transfer_public(address, u64)"
print(call.args) # ['aleo1…', '1000000u64'] — coercedYou can also list/iterate the available functions: list(credits.functions), "transfer_public" in credits.functions.
Before proving or broadcasting anything, build the authorization locally and look at what the call will produce. This is a proof-free, network-free dry run:
auth = call.simulate(account) # alias of .authorize(); .call() also works
print(auth.outputs) # per-transition output lists
print(auth.decoded()) # [{program, function, inputs, outputs}, …]Both AuthorizationResult and TransactionResult expose the same .outputs / .decoded() surface, plus a .raw escape hatch to the underlying network object. You can also decode after the fact with aleo.decode_transition(tx_id_or_transition).
build_transaction (alias prove) runs the whole ladder locally: authorize → execute → prepare trace → prove execution → authorize+prove fee → assemble. transact does that and broadcasts, returning the transaction id:
# requires a live node + funded private key
tx_id = credits.functions.transfer_public(
str(account.address), 100
).transact(account)
confirmed = aleo.network.wait_for_transaction(tx_id, timeout=60.0)Fees are public by default (base cost from the execution). Pass priority_fee= for a tip, or opt into a private fee with private_fee=True (auto-sourced from aleo.record_provider) or by passing an explicit fee_record=.
delegate hands proving to a Delegated Proving Service (DPS): you build only the lightweight main authorization locally, and the DPS does the expensive SNARK proving. By default the prover's fee master pays the fee — no records, no public fee, no friction. That frictionlessness is the whole point.
# requires prover credentials; fee master pays
result = credits.functions.transfer_public(
str(account.address), 100
).delegate(account)Want to pay your own fee instead? delegate(account, pay_own_fee=True) (public) or delegate(account, fee_record=<credits record>) (private). Both bind the fee to the real execution id, so they prove the execution locally first. broadcast=False returns the proven transaction without submitting it.
A full private transfer combines the two trust-minimising paths: delegated proving (the prover's fee master pays) and a record provider to discover the private records you own. The default record provider (aleo.records) uses Provable's hosted scanner — registering shares your view key with that service so it can index your records. If you'd rather not share a view key, assign your own RecordProvider instead.
# requires prover credentials + hosted-scanner registration
credits = aleo.programs.get("credits.aleo")
# 1) Move public credits into a private record (delegated — fee master pays)
credits.functions.transfer_public_to_private(str(account.address), 100_000) \
.delegate(account)
# 2) Register with the record provider and find the new private record
aleo.records.register(account) # shares the view key with the scanner
record = aleo.records.get_unspent_credits_record()
# 3) Spend the private record with a private transfer (delegated)
credits.functions.transfer_private(record, str(recipient.address), 1) \
.delegate(account)aleo.record_provider is swappable: set it to your own object implementing the RecordProvider protocol (get_unspent_credits_record / find) — e.g. a self-hosted scanner — and the whole facade (including private-fee auto-sourcing) uses it, with no view-key sharing.
The async facade mirrors the sync surface. Construction and the local, CPU-bound steps stay synchronous; everything that touches the network is awaitable.
import asyncio
from aleo import AsyncAleo
async def main():
aleo = AsyncAleo(AsyncAleo.HTTPProvider("https://api.provable.com/v2"))
print(aleo.network_name) # sync — no I/O
# Account ops are sync (purely local), even on AsyncAleo
account = aleo.account.create()
sig = aleo.account.sign(b"hi", account)
assert aleo.account.verify(str(account.address), b"hi", sig)
# Reads are awaited # requires a live node
micro = await aleo.get_balance(str(account.address))
print(aleo.from_microcredits(micro), "credits")
# Build a call — fetching the program is awaited # requires a live node
credits = await aleo.programs.get("credits.aleo")
call = credits.functions.transfer_public(str(account.address), 100)
# authorize / simulate / call are SYNC (local proof-free build)
auth = call.simulate(account)
print(auth.decoded())
# transact / delegate are awaited # requires a live node / prover creds
tx_id = await call.transact(account)
result = await call.delegate(account) # fee master pays by default
asyncio.run(main())Sync vs async on the async facade:
- Sync (local, no I/O):
account.*(create/import/sign/verify),to_microcredits/from_microcredits/is_valid_address, andauthorize/simulate/callon a bound call. - Async (awaitable):
is_connected,get_balance,programs.get, mapping reads,build_transaction/transact/delegate. Heavy proving runs inasyncio.to_threadso it does not block the event loop.
The SDK ships an eth-tester-style harness for fast, deterministic local testing.
Devnode launches a local aleo-devnode with manual block creation, so tests control exactly when blocks are produced. It auto-picks a free port and tears the node down on exit.
from aleo.testing import Devnode
with Devnode() as dn:
aleo = dn.aleo # an Aleo client wired to the devnode
alice = dn.accounts[0] # 5 deterministic, pre-funded genesis accounts
tx_id = aleo.programs.get("credits.aleo").functions.transfer_public(
str(dn.accounts[1].address), 1_000_000
).transact(alice)
dn.advance(1) # produce 1 block to confirm the tx
snap = dn.snapshot() # capture chain statedn.aleo— anAleoclient pointed at the devnode.dn.accounts— 5 deterministic, pre-funded genesis accounts.dn.advance(n)— producenblocks (the node runs with manual block creation).dn.snapshot()— capture the current chain state.
Requires the aleo-devnode binary on your PATH, or set $ALEO_DEVNODE_BIN to its location.
The -m slow live tests hit a real testnet and skip automatically when their environment variables are unset:
| Variable | Purpose |
|---|---|
ALEO_E2E_PRIVATE_KEY |
A funded testnet private key |
ALEO_E2E_ENDPOINT |
Node/API endpoint to test against |
ALEO_E2E_API_KEY |
Provable API key |
ALEO_E2E_CONSUMER_ID |
DPS consumer id for delegated proving |
The -m devnode tests additionally require the aleo-devnode binary (see Devnode above).
When you need direct control, import the network module and use the cryptographic types directly:
from aleo.mainnet import PrivateKey, Signature
pk = PrivateKey.random()
print(pk.address, pk.view_key)
sig = Signature.sign(pk, b"hello")
assert sig.verify(pk.address, b"hello")aleo.mainnet (and aleo.testnet, when the extension is compiled) expose the full type set — Account, Program, Process, Authorization, Transaction, RecordPlaintext, Field, Address, and more.
- sdk: A library that brings Aleo MainnetV0 functionalities to Python developers.
- zkml: A transpiler library that converts Python machine learning models into Leo code.
- zkml-research: Research on accurate/constraint-efficient zkML techniques, mostly for internal purposes.
For detailed information on each codebase, please navigate to their respective folders.