APlane Python SDK
Python SDK for signing Algorand transactions via apsigner.
Versioning
SDK packages are published only when the SDK changes. SDK versions track compatible APlane release tags and may skip product release numbers.
The native-Falcon SDK release line requires an APlane signer release after
v0.35.0; see docs/COMPATIBILITY.md.
Installation
pip install aplanesdk
The published package is aplanesdk on PyPI.
For the AlgoKit adapter, install AlgoKit Utils in the same environment:
pip install 'algokit-utils>=5.0.0b1'
Or install from source:
cd python
pip install -e .
Quick Start
from aplanesdk import SignerClient, send_raw_transaction
from algosdk import transaction
from algosdk.v2client import algod
# Connect to signer (reads config.yaml and token from data dir)
client = SignerClient.from_env()
# Build transaction with algosdk
algod_client = algod.AlgodClient("", "https://testnet-api.4160.nodely.dev")
params = algod_client.suggested_params()
txn = transaction.PaymentTxn(
sender="SENDER_ADDRESS",
sp=params,
receiver="RECEIVER_ADDRESS",
amt=1000000 # 1 ALGO
)
# Sign via apsigner (waits for operator approval)
signed = client.sign_transaction(txn)
# Submit to network (signed is ready to use, no processing needed)
txid = send_raw_transaction(algod_client, signed)
print(f"Submitted: {txid}")
Connection Methods
Data-directory connections use endpoints.yaml. SSH endpoints create a
managed tunnel; HTTPS and loopback HTTP endpoints connect directly.
Managed SSH keys and host trust live under the APlane client data directory;
the SDK does not use the operating-system user's personal SSH directory.
Environment-Based Connection (Recommended)
Load configuration from a data directory. The directory is required — pass
data_dir or set the APCLIENT_DATA environment variable:
# Set environment variable
# export APCLIENT_DATA=~/aplane/apclient
client = SignerClient.from_env()
# Or pass directly
client = SignerClient.from_env(data_dir="~/aplane/apclient")
Data directory structure (installer default: ~/aplane/apclient):
<data_dir>/
config.yaml # Non-routing client settings
endpoints.yaml # Signer and sentry routing
aplane.token # Authentication token
.ssh/
id_ed25519 # SSH private key for authentication
known_hosts # Trusted server host keys
Example endpoints.yaml:
schema_version: 2
default: primary
endpoints:
primary:
role: signer
url: ssh://localhost:1127
signer_port: 11270
identity_file: .ssh/id_ed25519
known_hosts_path: .ssh/known_hosts
Direct SSH Connection
Connect explicitly via SSH tunnel with 2FA:
client = SignerClient.connect_ssh(
host="signer.example.com",
token="your-token", # used for both SSH auth and HTTP API
ssh_key_path="~/aplane/apclient/.ssh/id_ed25519",
known_hosts_path="~/aplane/apclient/.ssh/known_hosts",
ssh_port=1127, # default: 1127
signer_port=11270, # default: 11270
timeout=30 # optional explicit shorter request timeout
)
Note: SSH verifies the enrolled public key, then performs a programmatic
mutual proof of the token bound to the accepted host key and fresh nonces. The
SSH uses the fixed product username default; the bearer token is never sent
as SSH metadata. Keys are enrolled via the request-token:default
operator-approved flow.
The SSH tunnel is established automatically. Remember to close when done:
client.close()
Or use as a context manager:
with SignerClient.connect_ssh(
host="...",
token="...",
ssh_key_path="~/aplane/apclient/.ssh/id_ed25519",
known_hosts_path="~/aplane/apclient/.ssh/known_hosts",
) as client:
signed = client.sign_transaction(txn)
# Tunnel closed automatically
Authentication
The recommended way to obtain a token is via the endpoint-based
request-token flow. request_token_to_file(endpoint="sentry.qa") selects a
named endpoint; without an alias it selects the default signer. The selected
endpoint determines the token destination.
If your token was provisioned separately (e.g. copied by the operator), you can load it explicitly:
from aplanesdk import load_token
token = load_token("/path/to/apclient/aplane.token")
API Reference
SignerClient
health() -> bool
Check if signer is reachable.
if client.health():
print("Signer is online")
get_status() -> StatusResponse
Fetch authenticated signer status. This works while the signer is locked.
identity = client.get_status()
print(identity.state, identity.keyset_revision)
keyset_revision is process-local and useful for deciding when to refresh
list_keys(refresh=True); it is not durable across apsigner restarts.
approval_wait_seconds is used by the SDK to size /sign deadlines.
list_keys() -> List[KeyInfo]
List available signing keys.
keys = client.list_keys()
for key in keys:
print(f"{key.address} [{key.key_type}]")
Returns list of KeyInfo:
address: Algorand addresskey_type: "ed25519", "aplane.falcon1024.v1", "aplane.htlc.v1", etc.logic_sig_resources: independent program-byte, argument-byte, and maximum-opcode-cost demand by authorization path.is_generic_lsig: True if no cryptographic signature neededsigning_args: List ofSigningArgfor LogicSigs (name, arg_type, description)
The SDK exposes bounded inventory and ordinary spend signing only. It does not
build, partially sign, or complete contract-admin rekey transactions; use the
APlane aprekey workflow for those operations.
Discovering required arguments for generic LogicSigs:
key_info = client.get_key_info(hashlock_address)
if key_info.signing_args:
for arg in key_info.signing_args:
print(f"{arg.name}: {arg.arg_type} - {arg.description}")
sign_transaction(txn, auth_address=None, lsig_args=None) -> str
Sign a single transaction. Returns a base64-encoded string ready for submission.
The server automatically handles fee pooling for large LogicSigs (e.g., Falcon-1024) by adding dummy transactions as needed.
# Basic signing (uses txn.sender as auth_address)
signed = client.sign_transaction(txn)
# Rekeyed account (different auth key)
signed = client.sign_transaction(txn, auth_address="SIGNER_KEY_ADDRESS")
# Generic LogicSig with runtime args (e.g., HTLC)
signed = client.sign_transaction(
txn,
auth_address="HASHLOCK_ADDRESS",
lsig_args={"preimage": b"secret_value"}
)
# Submit directly (no processing needed)
txid = send_raw_transaction(algod_client, signed)
sign_transactions(txns, auth_addresses=None, lsig_args_map=None) -> str
Sign multiple transactions as a group. Returns a base64-encoded string of concatenated signed transactions, ready for submission.
Important: Do NOT pre-assign group IDs. The server computes the group ID after adding any required dummy transactions for large LogicSigs.
# Build transactions (do NOT call assign_group_id)
txn1 = transaction.PaymentTxn(sender=addr1, sp=params, receiver=addr2, amt=100000)
txn2 = transaction.PaymentTxn(sender=addr2, sp=params, receiver=addr1, amt=100000)
# Sign group (server handles grouping and dummies)
signed = client.sign_transactions([txn1, txn2])
# Submit directly (no processing needed)
txid = algod_client.send_raw_transaction(signed)
sign_transactions_list(txns, auth_addresses=None, lsig_args_map=None) -> List[str]
Like sign_transactions() but returns individual base64-encoded transactions instead of concatenated. Useful when you need to inspect transactions individually.
signed_list = client.sign_transactions_list([txn1, txn2])
# signed_list is List[str], each element is a base64-encoded signed transaction
sign_requests(sign_entries, request_id=None) -> GroupSignResponse
Send one or more raw /sign request entries. Use this when an integration
already owns transaction encoding and wants APlane's native response shape.
response = client.sign_requests(
[{
"txn_bytes_hex": "5458...",
"auth_address": "SIGNER_KEY_ADDRESS",
"txn_sender": "SENDER_ADDRESS", # advisory display hint only
}],
request_id="app-owned-request-id",
)
AlgoKit Utils Adapter
For AlgoKit Utils 4 (utils-py v5) transaction composers, use the adapter
account. It connects AlgoKit clients to APlane's transaction signing functions
and presents the addr + signer(txn_group, indexes_to_sign) shape.
The minimal repository example is examples/algokit_self_send.py. From a
checkout, run it as a module so it imports the local SDK source:
cd ~/aplanesdk/python
export APCLIENT_DATA=~/aplane/apclient
export APLANE_ADDRESS=SENDER_ADDRESS
python -m examples.algokit_self_send
The example builds a transaction with AlgoKit, signs it through the APlane adapter, then submits the signed blobs with AlgoKit's algod client:
from algokit_utils import AlgoAmount, AlgorandClient, PaymentParams
from aplanesdk import SignerClient
from aplanesdk.algokit import create_apsigner_account
sender = "SENDER_ADDRESS"
algorand = AlgorandClient.testnet()
with SignerClient.from_env() as signer:
auth = algorand.client.algod.account_information(sender).auth_addr or sender
account = create_apsigner_account(signer, sender, auth_address=auth)
txn = algorand.create_transaction.payment(
PaymentParams(
sender=sender,
signer=account,
receiver=sender,
amount=AlgoAmount(micro_algo=0),
validity_window=1000,
)
)
signed = account.signer([txn], [0])
tx_id = algorand.client.algod.send_raw_transaction(signed).tx_id
Use create_transaction.* when APlane must own final signing and any
APlane-managed group expansion. algorand.send.* owns the composer send path
and signs inside that path.
The Python AlgoKit signer is synchronous. ApsignerAccount tracks one active
signing request at a time; overlapping calls on the same account raise
RuntimeError. Use separate account objects for concurrent signing. For
asyncio applications, run the AlgoKit call site in a worker thread, for example
with asyncio.to_thread(...). If an application needs to own request IDs, pass
new_request_id, a callable that returns a fresh ID for each sign call.
Signing calls discover /status.approval_wait_seconds and use that value
plus 30 seconds of slack for the request timeout. If discovery fails or an older
signer omits the field, signing falls back to 6 minutes. An explicit shorter
timeout still wins; SDK /sign calls include a request_id and send a
best-effort /sign/cancel when the HTTP request times out or disconnects.
High-level signing methods accept an optional keyword-only request_id.
AlgoKit adapter callers can call account.cancel() from another thread to
cancel the in-flight adapter request.
cancel_sign_request(request_id) -> CancelSignResponse
Ask apsigner to cancel a live synchronous /sign request by request ID.
Successful responses are idempotent for client behavior and return state
"canceled" or "not_found".
Python high-level signing generates a request ID by default. Interactive
applications can pass an application-owned ID, then call
cancel_sign_request() with the same value from another thread:
request_id = "wallet-ui-approval-123"
signed = client.sign_transaction(txn, request_id=request_id)
# elsewhere, if the user aborts while approval is pending:
client.cancel_sign_request(request_id)
For AlgoKit adapter signing, call cancel() on the account:
account.cancel()
close()
Close the client and SSH tunnel (if any).
client.close()
Supported Key Types
| Key Type | Description | Notes |
|---|---|---|
ed25519 |
Native Algorand keys | Standard signing |
aplane.falcon1024.v1 |
Post-quantum LogicSig | Signature in LogicSig.Args[0] |
aplane.ed25519.v1 |
Ed25519 DSA LogicSig | Library-visible plain DSA account |
aplane.witness-falcon1024.v1 |
Witness key | Sentry-custodied policy signature key; not a spending account |
aplane.falcon1024-sentry1024.v1 |
Guarded account | Requires user and sentry component signatures |
aplane.corridor.v1 |
Bounded Corridor account | bounded1 contract; bounded-sentry1 spend flow |
aplane.falcon1024-allowlist.v1 |
Bounded allowlist | Inline allowlist; bounded1 signing flow |
aplane.falcon1024-allowlist.v2 |
Bounded allowlist | Merkle allowlist; bounded1 signing flow |
aplane.falcon1024-timelock.v1 |
Bounded timelock | Round-gated bounded1 signing flow |
aplane.falcon1024-allowlist-alock.v1 |
Rekey-locked bounded allowlist | Ordinary spending uses bounded1; admin rekey is outside SDK scope |
aplane.htlc.v1 |
Hash-locked funds | Requires preimage arg (check signing_args) |
The server assembles the complete signed transaction - the SDK returns a base64 string ready for submission.
Sentry And Guarded Accounts
Witness keys enrolled as sentries are public policy-signature selectors, not Algorand spending accounts. Do not use them as senders, receivers, auth addresses, or rekey targets. Guarded account keys must be signed through the guarded flow.
Low-level endpoint wrappers are available:
from aplanesdk import (
AssemblyRequest,
AssemblyTarget,
ComponentRequest,
COMPONENT_TARGET_KIND_SENTRY,
COMPONENT_TARGET_KIND_USER,
)
user_part = user_client.request_components(ComponentRequest(
group_bytes_hex=["5458..."],
targets=[{
"target_index": 0,
"kind": COMPONENT_TARGET_KIND_USER,
"auth_address": "GUARDED_ACCOUNT_ADDRESS",
}],
))
sentry_part = sentry_client.request_components(ComponentRequest(
group_bytes_hex=["5458..."],
targets=[{
"target_index": 0,
"kind": COMPONENT_TARGET_KIND_SENTRY,
"component_key": "SENTRY_COMPONENT_SELECTOR",
}],
))
assembled = user_client.request_assemble(AssemblyRequest(
group_bytes_hex=["5458..."],
targets=[AssemblyTarget(
target_index=0,
kind=ASSEMBLY_TARGET_KIND_GUARDED,
auth_address="GUARDED_ACCOUNT_ADDRESS",
user_signature=user_part.components[0]["signature"],
sentry_signature=sentry_part.components[0]["signature"],
)],
))
For the common explicit two-client flow, use sign_guarded_group. The direct
helper does not perform inventory discovery, so pass the reviewed spend-path
resource profile returned by list_keys():
result = sign_guarded_group(
user_client=user_client,
sentry_client=sentry_client,
sentry_component_key="SENTRY_COMPONENT_SELECTOR",
group_bytes_hex=["5458..."],
guarded_targets=[
GuardedSignTarget(
target_index=0,
guarded_account="GUARDED_ACCOUNT_ADDRESS",
logic_sig_resources=reviewed_spend_resources,
),
],
)
signed_group = result.signed_group
assemble_group() remains the local multi-party concatenation helper; it is
not the same operation as server-side guarded assembly.
Bounded Sentry Accounts
Corridor uses the bounded contract bounded1 with the distinct
bounded-sentry1 online signing flow. The contract identifies the LogicSig
rules; the flow identifies the user-first multi-endpoint choreography. The
prepared helper detects that flow from signer inventory and routes it
automatically:
result = sign_prepared_guarded_group(
user_client=user_client,
sentry_resolver=sentry_resolver,
prepared_group=prepared_group,
)
signed_group = result.signed_group
The SDK first freezes the complete canonical group through /plan; the user
signer then approves those bytes through request_components() with
kind="bounded-base". Only then does the SDK request sentry signatures over
the same bytes, sign ordinary positions, and call request_assemble(). Before
signing anything, the SDK compares the
signer-produced plan with the caller's prepared group: only reported fee
pooling and group-ID assignment are accepted, and appended positions must be
canonical budget dummies. The returned group must use canonical transaction
encoding and a group ID recomputed from the presented membership. The SDK also
verifies ordinary signed positions and every assembled transaction against the
frozen transaction bytes.
request_components() sends best-effort /sign/cancel when its
approval-bearing HTTP request times out or disconnects.
request_assemble() does not open an approval request and is not a
cancellation handle.
The signer planner owns fee selection, authorization-resource sizing, and any reported group mutations for both guarded flows.
Applications that own orchestration can call request_components() and
request_assemble() directly. Sentry authorization is spend-only in
this contract; bounded contract-admin rekeys remain an external aprekey
ceremony and are not completed by the SDK.
Error Handling
Signing Exceptions
from aplanesdk import (
SignerError,
AuthenticationError,
SigningRejectedError,
SignerUnavailableError,
KeyNotFoundError
)
try:
signed = client.sign_transaction(txn)
except AuthenticationError:
print("Invalid token")
except SigningRejectedError:
print("Operator rejected the request")
except SignerUnavailableError:
print("Signer not reachable or locked")
except KeyNotFoundError:
print("Key not found in signer")
except SignerError as e:
print(f"Signing failed: {e}")
Submission Exceptions
send_raw_transaction() wraps verbose algod errors into clean exceptions:
from aplanesdk import (
send_raw_transaction,
TransactionRejectedError,
LogicSigRejectedError,
InsufficientFundsError,
InvalidTransactionError
)
try:
txid = send_raw_transaction(algod_client, signed)
except LogicSigRejectedError as e:
print(f"LogicSig failed: {e.reason}") # e.txid also available
except InsufficientFundsError as e:
print(f"Not enough funds: {e.reason}")
except InvalidTransactionError as e:
print(f"Invalid transaction: {e.reason}")
except TransactionRejectedError as e:
print(f"Rejected: {e.reason}")
Example: Complete Workflow
#!/usr/bin/env python3
from aplanesdk import SignerClient, load_token, SignerError, send_raw_transaction
from algosdk import transaction
from algosdk.v2client import algod
def main():
# Load token
token = load_token("~/aplane/apclient/aplane.token")
# Connect via SSH (public key plus host-key-bound token proof)
with SignerClient.connect_ssh(
host="signer.example.com",
token=token,
ssh_key_path="~/aplane/apclient/.ssh/id_ed25519",
known_hosts_path="~/aplane/apclient/.ssh/known_hosts",
) as client:
# List keys
keys = client.list_keys()
sender = keys[0].address
print(f"Using: {sender}")
# Build transaction
algod_client = algod.AlgodClient("", "https://testnet-api.4160.nodely.dev")
params = algod_client.suggested_params()
txn = transaction.PaymentTxn(
sender=sender,
sp=params,
receiver=sender,
amt=0
)
# Sign (will wait for operator approval)
try:
signed = client.sign_transaction(txn)
print("Signed!")
# Submit directly (no processing needed)
txid = send_raw_transaction(algod_client, signed)
print(f"TxID: {txid}")
# Wait for confirmation
result = transaction.wait_for_confirmation(algod_client, txid, 4)
print(f"Confirmed in round {result['confirmed-round']}")
except SignerError as e:
print(f"Failed: {e}")
if __name__ == "__main__":
main()
LogicSig Resource Planning
The signer plans LogicSig program bytes, argument bytes, and opcode cost as independent consensus resources. Under v42, excess program bytes are paid by the group fee; dummies are added only when argument or opcode capacity requires them.
How It Works (Server-Side)
- Server selects the authorization-path resource profile.
- Server solves argument and opcode capacity, adding canonical dummies only when required.
- Server adds the v42 program-byte fee contribution.
- Server computes the final group ID and signs all transactions.
- SDK returns the complete signed group ready for submission.
Example: Falcon-1024 Key
params = algod_client.suggested_params()
txn = transaction.PaymentTxn(sender=falcon_addr, sp=params, receiver=receiver, amt=1000000)
# Server automatically adds dummies - just sign and submit
signed = client.sign_transaction(txn)
txid = send_raw_transaction(algod_client, signed)
Fee Impact
| Key Type | LogicSig Size | Dummies Needed | Extra Fee |
|---|---|---|---|
| Ed25519 | 0 | 0 | 0 |
| Falcon-1024 | ~3035 | 3 | ~3000 uA |
The extra fee covers the dummy transactions required for post-quantum security.
License
MIT
Project
This SDK is part of the APlane project:
- Repository: https://github.com/aplane-algo/aplanesdk
- SDK path:
python
APlane is an open-source project stewarded by the APlane Project.
See the repository README for project overview and alpha-status guidance, and DISCLAIMER.md for risk, liability, and usage information.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aplanesdk-0.36.0.tar.gz.
File metadata
- Download URL: aplanesdk-0.36.0.tar.gz
- Upload date:
- Size: 92.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1b0a9fb3e388781a4fa819b72ff5697e1979e48f274994386b3d1fa6ea7f282
|
|
| MD5 |
1ed5a81daa90c540df0ead6114fcbdf7
|
|
| BLAKE2b-256 |
54b475f6051f3012701cf47125e17408fd653273dd9c1c4c8a4f52baa66dfaaa
|
Provenance
The following attestation bundles were made for aplanesdk-0.36.0.tar.gz:
Publisher:
publish-sdks.yml on aplane-algo/aplanesdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aplanesdk-0.36.0.tar.gz -
Subject digest:
d1b0a9fb3e388781a4fa819b72ff5697e1979e48f274994386b3d1fa6ea7f282 - Sigstore transparency entry: 2518951464
- Sigstore integration time:
-
Permalink:
aplane-algo/aplanesdk@a22ce1182ac455c7140219b175f85f9837d25784 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/aplane-algo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-sdks.yml@a22ce1182ac455c7140219b175f85f9837d25784 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file aplanesdk-0.36.0-py3-none-any.whl.
File metadata
- Download URL: aplanesdk-0.36.0-py3-none-any.whl
- Upload date:
- Size: 62.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
27dfd02a6c795397d56a01755c6d74339ece22aeb7faaca5087a21d2158420e2
|
|
| MD5 |
cba7a8c2b52178b8adba259b32280e59
|
|
| BLAKE2b-256 |
56209aa23354ba941e0feab1cb318c678f14a53c1383fe0441a65744d131b29a
|
Provenance
The following attestation bundles were made for aplanesdk-0.36.0-py3-none-any.whl:
Publisher:
publish-sdks.yml on aplane-algo/aplanesdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aplanesdk-0.36.0-py3-none-any.whl -
Subject digest:
27dfd02a6c795397d56a01755c6d74339ece22aeb7faaca5087a21d2158420e2 - Sigstore transparency entry: 2518951997
- Sigstore integration time:
-
Permalink:
aplane-algo/aplanesdk@a22ce1182ac455c7140219b175f85f9837d25784 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/aplane-algo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-sdks.yml@a22ce1182ac455c7140219b175f85f9837d25784 -
Trigger Event:
workflow_dispatch
-
Statement type: