Your First Aptos Multisig (Python SDK)
In this tutorial, you’ll learn how to create and manage a multisig account that requires 2 out of 3 key holders to approve any transaction. You’ll learn how to:
- Set up a development environment for Aptos
- Create multiple accounts to act as key holders
- Configure a multisig account requiring 2-of-3 signatures
- Fund accounts and verify balances
- Create and execute multisig transactions
If you’re coming from Ethereum/Solidity, note that Aptos handles multisig accounts differently. Aptos implements multisig directly at the protocol level, allowing accounts to require multiple signatures without deploying additional smart contracts.
We’re interfacing with Aptos using the Aptos Python SDK.
Conceptually, a multisig (multi-signature) account works like a bank vault requiring multiple key holders to authorize access. In Aptos, this is implemented with digital signatures rather than physical keys, with each authorized signer providing their cryptographic approval.
Setup
First, let’s prepare our development environment. We’ll create an isolated workspace and install all necessary dependencies.
Open a terminal
Open a new terminal window.
Verify Python installation
Run this command to check your Python version:
python3 --version
You should see something like “Python 3.7” or higher.
If you see an error or your Python version is below version 3.7, download Python from python.org.
Create project directory
Create a new folder for our project:
mkdir my-first-multisig
Navigate to project directory
Move into this new folder:
cd my-first-multisig
Create virtual environment
Set up an isolated Python environment:
python3 -m venv venv
This command:
- Creates an isolated Python environment
- Installs a fresh Python instance
- Keeps project dependencies separate from your system Python
- Creates a
venv
folder (you can view but don’t modify its contents!)
Activate virtual environment
source venv/bin/activate
This command:
- Modifies your terminal’s environment variables
- Makes your terminal use the Python from
venv
instead of your system Python - You’ll see
(venv)
appear at the start of your terminal line - To deactivate later, simply type
deactivate
Install Aptos SDK
Install the required SDK:
pip install aptos-sdk
This command:
- Downloads the Aptos SDK package from PyPI (Python Package Index)
- Installs it inside your
venv
folder - Creates files in
venv/lib/python3.x/site-packages/aptos_sdk
- You can view these files by navigating to that directory
Creating the Foundation
Let’s start building our multisig implementation. First, we’ll set up our imports, main loop, and base configuration.
Create Python script
Create an empty Python script file:
touch multisig.py
Add base code
Open multisig.py
in your IDE (we recommend VSCode or JetBrains) and add the following code:
# Copyright © Aptos Foundation
# SPDX-License-Identifier: Apache-2.0
import asyncio
import subprocess
import time
from aptos_sdk.account import Account, RotationProofChallenge
from aptos_sdk.account_address import AccountAddress
from aptos_sdk.async_client import FaucetClient, RestClient
from aptos_sdk.authenticator import Authenticator, MultiEd25519Authenticator
from aptos_sdk.bcs import Serializer
from aptos_sdk.ed25519 import MultiPublicKey, MultiSignature
from aptos_sdk.transactions import (
EntryFunction,
RawTransaction,
Script,
ScriptArgument,
SignedTransaction,
TransactionArgument,
TransactionPayload,
)
from aptos_sdk.type_tag import StructTag, TypeTag
# Network configuration - using devnet for testing. Check current urls at:
# https://github.com/aptos-labs/aptos-python-sdk/blob/main/examples/common.py
NODE_URL = "https://fullnode.devnet.aptoslabs.com/v1"
FAUCET_URL = "https://faucet.devnet.aptoslabs.com"
should_wait = True
# "wait" is used to make the terminal more interactive, so it's easier to follow what is happening.
def wait():
"""Wait for user to press Enter before starting next section."""
if should_wait:
input("\nPress Enter to continue...")
# Now we define our main function which calls everything else.
# We will add all future additions inside this function.
async def main(should_wait_input=True):
# This is just used for this tutorial.
global should_wait
should_wait = should_wait_input
# Initialize our blockchain clients
rest_client = RestClient(NODE_URL)
faucet_client = FaucetClient(FAUCET_URL, rest_client)
############# Add additional code here ###############
######################################################
if __name__ == "__main__":
asyncio.run(main())
This code imports all the necessary modules from the Aptos SDK. The aptos_sdk.account
module provides essential functionality for managing accounts and signatures, while aptos_sdk.transactions
gives us the tools to create and submit blockchain transactions.
You can get free test tokens on Aptos Devnet or Testnet by using the FaucetClient
.
Creating Our Key Holders
Just like a bank vault needs designated key holders, our multisig needs authorized signers. Let’s create the accounts for our key holders.
Create key holder accounts
Add the following code after ############# Add additional code here ###############
:
# Create three accounts to act as our key holders
alice = Account.generate()
bob = Account.generate()
chad = Account.generate()
The Account.generate()
function creates a new Aptos account with a fresh keypair. Each account will have its own private key (for signing) and public key (for verification). In our multisig setup, these accounts represent the key holders who will have authorization to sign transactions, similar to how each bank vault key holder would have their own unique physical key.
Each time you run this script it will generate new accounts on the devnet. You’ll need to save the private key and account address if you want to continue working with that account.
Add account information display
Add this code below chad = Account.generate()
:
print("\n=== Account addresses ===")
print(f"Alice: {alice.address()}")
print(f"Bob: {bob.address()}")
print(f"Chad: {chad.address()}")
print("\n=== Authentication keys ===")
print(f"Alice: {alice.auth_key()}")
print(f"Bob: {bob.auth_key()}")
print(f"Chad: {chad.auth_key()}")
print("\n=== Public keys ===")
print(f"Alice: {alice.public_key()}")
print(f"Bob: {bob.public_key()}")
print(f"Chad: {chad.public_key()}")
wait()
# Add additional code below this wait()
Run the script
Run our multisig.py
from your terminal:
python3 multisig.py
You should see output showing the addresses, authentication keys, and public keys for each account. For example:
=== Account addresses ===
Alice: 0x5323a06f21b04af53fc57367b50d3bbb5675c655bc9bc62f33b5e083d5d06b8b
Bob: 0x9f3e94fc92e0076336c122a576304c0b9fa8def13a98c469dce05e0836b9fe5b
Chad: 0x1d0e7b790493dcf7bc7ce60bf4ccdcca1d38ce0d7f8dd26d2791a6d3ff6da708
=== Authentication keys ===
Alice: 0x5323a06f21b04af53fc57367b50d3bbb5675c655bc9bc62f33b5e083d5d06b8b
Bob: 0x9f3e94fc92e0076336c122a576304c0b9fa8def13a98c469dce05e0836b9fe5b
Chad: 0x1d0e7b790493dcf7bc7ce60bf4ccdcca1d38ce0d7f8dd26d2791a6d3ff6da708
=== Public keys ===
Alice: 0x730264a36d4ec90af2e28e1cf9c4d686440598317123469a7c827d4fcdf74715
Bob: 0xcf21e85337a313bdac33d068960a3e52d22ce0e6190e9acc03a1c9930e1eaf3e
Chad: 0xa1a2aef8525eb20655387d3ed50b9a3ea1531ef6117f579d0da4bcf5a2e1f76d
For each user, note the account address and authentication key are identical, but the public key is different.
The Aptos account model facilitates the unique ability to rotate an account’s private key. Since an account’s address is the initial authentication key, the ability to sign for an account can be transferred to another private key without changing its public address.
Configuring the Multisig Vault
Now that we have our key holders (Alice, Bob, and Chad), let’s set up our multisig configuration.
Configure multisig account
Add code to configure a 2-of-3 multisig account:
# Configure a 2-of-3 multisig account
threshold = 2
multisig_public_key = MultiPublicKey(
[alice.public_key(), bob.public_key(), chad.public_key()],
threshold
)
multisig_address = AccountAddress.from_key(multisig_public_key)
The threshold = 2
sets our requirement for two signatures out of three possible signers. The MultiPublicKey
combines all three public keys into a single multisig configuration.
This is like setting up a bank vault’s access rules: “Any two of these three people must approve to access the vault.”
Display multisig information
Print the multisig account information by adding this code below our newly defined multisig_address
:
print("\n=== 2-of-3 Multisig account ===")
print(f"Account public key: {multisig_public_key}")
print(f"Account address: {multisig_address}")
wait()
# Add additional code here
Run the script
Verify the output:
python3 multisig.py
You should see output showing your multisig account’s public key type and its unique address on the Aptos blockchain. For example:
=== 2-of-3 Multisig account ===
Account public key: 2-of-3 Multi-Ed25519 public key
Account address: 0x08cac3b7b7ce4fbc5b18bc039279d7854e4c898cbf82518ac2650b565ad4d364
Funding Our Accounts
Just like new bank accounts need initial deposits, our blockchain accounts need funds to operate.
Add funding code
Add code to fund all accounts:
print("\n=== Funding accounts ===")
alice_start = 10_000_000
bob_start = 20_000_000
chad_start = 30_000_000
multisig_start = 40_000_000
# Fund all accounts concurrently
alice_fund = faucet_client.fund_account(alice.address(), alice_start)
bob_fund = faucet_client.fund_account(bob.address(), bob_start)
chad_fund = faucet_client.fund_account(chad.address(), chad_start)
multisig_fund = faucet_client.fund_account(multisig_address, multisig_start)
await asyncio.gather(*[alice_fund, bob_fund, chad_fund, multisig_fund])
The fund_account()
function requests test tokens from the Aptos faucet to let us experiment without using real APT. We fund all accounts simultaneously rather than one at a time by first initializing them as [name]_fund
and then awaiting the async function call that gathers them: asyncio.gather()
.
Check balances
Add code to check all balances and print them out:
# Check all balances
alice_balance = rest_client.account_balance(alice.address())
bob_balance = rest_client.account_balance(bob.address())
chad_balance = rest_client.account_balance(chad.address())
multisig_balance = rest_client.account_balance(multisig_address)
[alice_balance, bob_balance, chad_balance, multisig_balance] = await asyncio.gather(
*[alice_balance, bob_balance, chad_balance, multisig_balance]
)
print(f"Alice's balance: {alice_balance}")
print(f"Bob's balance: {bob_balance}")
print(f"Chad's balance: {chad_balance}")
print(f"Multisig balance: {multisig_balance}")
wait()
The account_balance()
function queries the blockchain for each account’s current balance. Again, we use asyncio.gather()
to make all these queries efficiently in parallel.
Run the script
Verify funding success by running:
python3 multisig.py
The output should show each account with its respective balance. For example:
=== Funding accounts ===
Alice's balance: 10000000
Bob's balance: 20000000
Chad's balance: 30000000
Multisig balance: 40000000
If any balance shows as 0, you may need to rerun the funding command as the faucet occasionally has temporary issues.
Values are in octas (1 APT = 100_000_000 octas). This is similar to how 1 dollar = 100 cents.
Creating Our First Multisig Transaction
Now let’s create a transaction that requires multiple signatures. We’ll transfer 100 octas from the multisig account to Chad, similar to how a bank transfer would require two managers to approve a large withdrawal.
Create transfer transaction
Create the transfer transaction by defining its parameters:
# Create the transfer transaction
entry_function = EntryFunction.natural(
module="0x1::coin",
function="transfer",
ty_args=[TypeTag(StructTag.from_str("0x1::aptos_coin::AptosCoin"))],
args=[
TransactionArgument(chad.address(), Serializer.struct),
TransactionArgument(100, Serializer.u64),
],
)
# Build the raw transaction
chain_id = await rest_client.chain_id()
raw_transaction = RawTransaction(
sender=multisig_address,
sequence_number=0,
payload=TransactionPayload(entry_function),
max_gas_amount=2000,
gas_unit_price=100,
expiration_timestamps_secs=int(time.time()) + 600,
chain_id=chain_id,
)
The code above:
- Uses
EntryFunction.natural()
to create a transfer of 100 octas (APT’s smallest unit) to Chad’s address - Sets up transaction parameters like gas limits and expiration time
- Creates a raw transaction that still needs signatures before it can be submitted
Get signatures
Get signatures from Alice and Bob:
alice_signature = alice.sign(raw_transaction.keyed())
bob_signature = bob.sign(raw_transaction.keyed())
print("\n=== Individual signatures ===")
print(f"Alice: {alice_signature}")
print(f"Bob: {bob_signature}")
wait()
The above code:
- Has Alice sign the transaction with her private key
- Has Bob sign the same transaction with his private key
- Prints the signatures to verify they were created successfully
Run the script
After you add the code for creating the transaction and getting signatures, run the script:
python3 multisig.py
You should see something like:
=== Individual signatures ===
Alice: 0x360e66c75b1ba787ec7b05998cbc14276d7fc0c006fb10c33d5cc3c4cc2ec4f53a8c0996b8e746fd6d86b09b4f8bb128cbf62d8b375f5b974faae040e889ac0d
Bob: 0xdcfd1965e531deb79de9d8daf7f28f46023107ce4f11612ce76da33e808486a0a368b34563d4f89d6179a3957a266c1e8809691fddabba3c2a3d8be14d6f2f0c
This shows that both Alice and Bob have signed the transaction. Each signature is a unique hash that proves they authorized the transaction with their private keys.
Like gathering two bank managers to sign a withdrawal slip - we need both signatures before the transaction can proceed.
Changing the number of managers required from two out of three to 13 out of 22 (or any K-of-N your business needs) is a few more lines of code.
Submitting the Multisig Transaction
Now we’ll combine the signatures and submit the transaction. This is similar to gathering all the signed papers from bank managers and submitting them to process a large transfer.
Combine signatures
Combine the signatures into a multisig authenticator:
# Combine the signatures (map from signatory public key index to signature)
sig_map = [(0, alice_signature), (1, bob_signature)]
multisig_signature = MultiSignature(sig_map)
# Create the authenticator with our multisig configuration
authenticator = Authenticator(
MultiEd25519Authenticator(multisig_public_key, multisig_signature)
)
The sig_map
links each signer’s public key to their signature, proving that both Alice and Bob have approved this transaction. The MultiSignature
and Authenticator
objects package these signatures in a format the blockchain can verify.
Submit transaction
Create and submit the signed transaction:
# Create and submit the signed transaction
signed_transaction = SignedTransaction(raw_transaction, authenticator)
print("\n=== Submitting transfer transaction ===")
tx_hash = await rest_client.submit_bcs_transaction(signed_transaction)
await rest_client.wait_for_transaction(tx_hash)
print(f"Transaction hash: {tx_hash}")
The SignedTransaction
combines the original transaction data with the authenticator proving both required signatures are present. We then submit this to the blockchain using submit_bcs_transaction()
and wait for confirmation.
Check new balances
Check the new account balances after transaction:
print("\n=== New account balances ===")
[alice_balance, bob_balance, chad_balance, multisig_balance] = await asyncio.gather(
*[
rest_client.account_balance(alice.address()),
rest_client.account_balance(bob.address()),
rest_client.account_balance(chad.address()),
rest_client.account_balance(multisig_address),
]
)
print(f"Alice's balance: {alice_balance}")
print(f"Bob's balance: {bob_balance}")
print(f"Chad's balance: {chad_balance}")
print(f"Multisig balance: {multisig_balance}")
Run the script
To see the transaction results, run:
python3 multisig.py
You should see something like:
=== Submitting transfer transaction ===
Transaction hash: 0x2f0b7fc8e69213f0c7e720e660f789b6e3d3564729a298f2b4f6794245833f2d
=== New account balances ===
Alice's balance: 10000000
Bob's balance: 20000000
Chad's balance: 30000100 # Increased by 100 octas
Multisig balance: 39999200 # Decreased by 100 octas plus gas fees
Notice how:
- Chad’s balance increased by exactly 100 octas, but Alice and Bob’s balances didn’t change since they only signed
- The multisig account paid for both the transfer amount and the gas fees
You can verify transaction on Aptos Explorer:
- Go to Aptos Explorer
- Make sure Explorer is set to Devnet (check the top right corner)
- Search for your multisig address or transaction hash
- Review the transaction details and balance changes
Going Further: Advanced Features
You’ve completed the basics of Aptos multisig - creating a “vault” (multisig account), adding “key holders” (signers), and making a simple transfer that requires multiple approvals. But just like modern banking, there’s much more we can do:
Vanity Addresses
Like having a custom bank account number, Aptos lets you create “vanity” addresses that start with specific characters. Imagine being able to choose a memorable account number like “0xdd…” for your company “Digital Dynamics”!
Account Rotation
Banks let you update your security credentials without changing your account number. Similarly, Aptos multisig accounts can “rotate” their authentication keys while keeping the same address - perfect for updating security without disrupting existing payment setups.
Governance & Smart Contracts
Just as banks have complex approval systems for large corporate accounts, Aptos multisig can interact with smart contracts and governance systems. Imagine setting up automated rules like:
- Required approvals based on transaction size
- Time-locked transactions
- Integration with DAO voting systems
Let us know what excites you most about multisig on Aptos! Join our community channels to share your ideas and experiences.
Next Steps
- Review the complete code example which include all the Advanced Features (see above).
- Learn about multisig governance in this tutorial.
- Explore account abstraction in Aptos.
- Join the Aptos Discord for developer support.