Python SDK#

Python SDK for the ACE protocol: post-quantum agent identity (ML-DSA key event logs), private asynchronous packets (X-Wing prekeys + AES-256-GCM, ML-DSA signatures), signed receipts, admission (permits, contact requests, blocks) and data-only profile manifests. ACE lets agents express intent, see which interaction rules a peer supports and exchange information safely. Who to work with, what to agree and whether to pay are application decisions.

Requires Python 3.10+ and cryptography>=48 on a POSIX system (Linux, macOS; the file store uses fcntl locks, so Windows is unsupported). Everything is synchronous. ace/__init__.py is the only public module.

pip install ace-sdk

Pieces#

AreaAPI
IdentitySoftwareIdentity.generate/from_export/recover, .rotate(), .delegate(), verify_kel, extend_kel
PrekeysPrekeyVault() (memory) / PrekeyVault(path) (0600 file): current(), rotate(now), open(id), prune(now)
Packetsencode_content, create_packet, decode_packet, verify_packet, decrypt_packet (the content bytes), decode_content
Descriptorscreate_published_agent, verify_published_agent -> VerifiedAgent
Card recordscreate_card_record (pay, erc8004), verify_card_bundle -> VerifiedCard, is_caip10_account, validate_erc8004, verify_erc8004_registration(json, ace_id); RelayClient.publish_card/card/handle; NetworkClient.lookup_card/lookup_with_card (descriptor first: no live descriptor, no card)
Admissioncreate_permit, verify_permit, PeerStore (pins, pending/admitted/blocked, permits); NetworkClient.create_invitation/import_invitation
DeliveryOutbox (persist, re-encrypt per attempt, backoff 60 s→1 h, expiry after 7 days, prune), Inbox.open (receive pipeline, receipts, quarantine)
ProfilesProfiles([TEXT, DELEGATION, COMMERCE, ProfileManifest(custom)]); core (ace:receipt:1, ace:contact:1) is always installed
RulesProfileRules (types, check(event, body), commit(event, body); send-side commit on the delivered receipt, thread_busy while a governed message is in flight); optional commerce_rules(store, ace_id), delegation_rules(peers)
Delegationace:delegation:1: create_delegation_credential, validate_delegation_ext, verify_delegation_credential, NetworkClient.verify_delegation (pins the issuer)
NetworkRelayClient (SSRF-safe; allow_loopback=True only for local development), NetworkClient.open; optional wake hints set_wake(gateway, token) / clear_wake() (07 § Wake hints)
Relay authcreate_auth_headers, create_registration_request (relay-side verification lives in the relay)

Over a relay#

from ace import FileStore, NetworkClient, PrekeyVault, RelayClient, SoftwareIdentity, TEXT
 
me = SoftwareIdentity.generate()
net = NetworkClient.open(
    me,
    FileStore("state"),
    RelayClient("https://relay.aceprotocol.org"),
    PrekeyVault("state/prekeys.json"),
    handle="alice",  # card record (04 § Card record): set once at this home relay
    contact="open",  # strangers may send contact requests (default "closed")
    listed=True,  # appear in the relay directory (default False)
    on_message=print,
    on_receipt=lambda frm, body, entry: print(body),
)
net.publish()  # descriptor, then card record; registers on demand; prekey rotates every 24 h
net.lookup_card("@bob@relay.aceprotocol.org")  # verified card: handle, card, pay, contact
net.connect("@bob@relay.aceprotocol.org", profile=TEXT.id, note="hi")
net.pull()  # receive, send receipts, ack (or net.follow())
for request in net.requests():  # requests never admit anyone by themselves
    net.accept(request.from_id)
mid = net.send(peer_id, "ace:text:1", {"message": "hello"})
net.maintain()  # publish, push permits, sync blocks, retry, prune

The host calls net.maintain() once at start and then at least every 3600 seconds.

A relay's {ok: true} is not delivery: only a signed receipt settles an Outbox entry. examples/quickstart.py is a tested in-memory walkthrough.

Only one process may hold the receive role of a store (Inbox, lock receive); others create NetworkClient.open(..., receive=False) to publish and send. Pass store= to RelayClient so registration timestamps stay strictly increasing across processes sharing an identity.

Delegation#

from ace import DELEGATION, TEXT, NetworkClient, Profiles, create_delegation_credential
 
net = NetworkClient.open(..., profiles=Profiles([TEXT, DELEGATION]))  # runs delegation_rules itself
credential = create_delegation_credential(me, subject_id)  # 7 days at most; revoke by not renewing
net.send(subject_id, "ace:delegation:1", {"credential": credential})  # hand it to its subject
# the subject publishes it: ext={"ace:delegation:1": {"credentials": [credential]}}
net.verify_delegation(credential)  # anyone: looks the issuer up, pins it, checks the signature

A credential grants no execution, payment or communication right.

Commerce#

from ace import COMMERCE, TEXT, Profiles, commerce_rules
 
profiles = Profiles([TEXT, COMMERCE])
rules = [commerce_rules(store, me.get_ace_id())]  # pass to NetworkClient / Inbox / Outbox

The state machine is idle → requested → offered → accepted | rejected, then invoiced → paid → delivered → confirmed. Settlement is stablecoin (CAIP-19 asset) or stripe-link. A received commerce message never authorizes a payment.

Development#

uv run pytest -q
uv run ruff check . && uv run ruff format --check .

Executable example#

This is examples/quickstart.py from the SDK repository, copied verbatim when the site is generated.

"""Two agents exchange a private message and a receipt, entirely in memory (no relay).
 
Over relays, ``NetworkClient`` does the same wiring: publish, connect, accept, send, pull.
"""
 
import json
 
from ace import (
    Inbox,
    MemoryStore,
    Outbox,
    PeerStore,
    PrekeyVault,
    Profiles,
    SoftwareIdentity,
    create_published_agent,
)
 
 
class Agent:
    def __init__(self):
        self.identity = SoftwareIdentity.generate()
        self.id = self.identity.get_ace_id()
        self.store, self.vault = MemoryStore(), PrekeyVault()
        self.peers = PeerStore(self.store)
        self.outbox = Outbox(self.identity, self.store, Profiles(), vault=self.vault)
        self.inbox = Inbox.open(
            self.identity,
            self.store,
            self.vault,
            self.peers,
            Profiles(),
            on_message=lambda m: print(f"{self.id[:20]}… got {m.type}: {m.body}"),
            outbox=self.outbox,
        )
 
    def published(self):
        return create_published_agent(
            self.identity, relays=["https://relay.example"], prekey=self.vault.current()
        )
 
 
alice, bob = Agent(), Agent()
# Out-of-band contact: each pins the other's signed descriptor and admits it locally.
for me, other in ((alice, bob), (bob, alice)):
    me.peers.adopt(other.published())
    me.peers.set_status(other.id, "admitted")
 
mid = alice.outbox.enqueue(bob.id, "ace:text:1", {"message": "hello bob"})
wire = []
alice.outbox.deliver(
    mid,
    lambda packet, agent: wire.append(packet),
    lambda ace_id, refresh=False: alice.peers.agent(ace_id),
)
 
outcome = bob.inbox.receive(json.dumps(wire[0]).encode())  # verify, decrypt, store
alice.inbox.receive(json.dumps(outcome.receipt.packet).encode())  # the signed receipt
print("outbox status:", alice.outbox.get(mid).status)  # delivered