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
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.
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:
Terminal window python3 --versionTerminal window python --versionYou should see something like “Python 3.7” or higher.
-
Create project directory
Create a new folder for our project:
Terminal window mkdir my-first-multisig -
Navigate to project directory
Move into this new folder:
Terminal window cd my-first-multisig -
Create virtual environment
Set up an isolated Python environment:
Terminal window python3 -m venv venvTerminal window python -m venv venvThis 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
Terminal window source venv/bin/activateTerminal window .\venv\Scripts\activateThis 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:
Terminal window pip install aptos-sdkThis 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
Section titled “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:
Terminal window touch multisig.pyTerminal window echo "" > multisig.py -
Add base code
Open
multisig.py
in your IDE (we recommend VSCode or JetBrains) and add the following code:Apache-2.0 # Copyright © Aptos Foundationimport asyncioimport subprocessimport timefrom aptos_sdk.account import Account, RotationProofChallengefrom aptos_sdk.account_address import AccountAddressfrom aptos_sdk.async_client import FaucetClient, RestClientfrom aptos_sdk.authenticator import Authenticator, MultiEd25519Authenticatorfrom aptos_sdk.bcs import Serializerfrom aptos_sdk.ed25519 import MultiPublicKey, MultiSignaturefrom 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.pyNODE_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_waitshould_wait = should_wait_input# Initialize our blockchain clientsrest_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, whileaptos_sdk.transactions
gives us the tools to create and submit blockchain transactions.
Creating Our Key Holders
Section titled “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 holdersalice = 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. -
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:Terminal window python3 multisig.pyTerminal window python multisig.pyYou should see output showing the addresses, authentication keys, and public keys for each account. For example:
Terminal window === Account addresses ===Alice: 0x5323a06f21b04af53fc57367b50d3bbb5675c655bc9bc62f33b5e083d5d06b8bBob: 0x9f3e94fc92e0076336c122a576304c0b9fa8def13a98c469dce05e0836b9fe5bChad: 0x1d0e7b790493dcf7bc7ce60bf4ccdcca1d38ce0d7f8dd26d2791a6d3ff6da708=== Authentication keys ===Alice: 0x5323a06f21b04af53fc57367b50d3bbb5675c655bc9bc62f33b5e083d5d06b8bBob: 0x9f3e94fc92e0076336c122a576304c0b9fa8def13a98c469dce05e0836b9fe5bChad: 0x1d0e7b790493dcf7bc7ce60bf4ccdcca1d38ce0d7f8dd26d2791a6d3ff6da708=== Public keys ===Alice: 0x730264a36d4ec90af2e28e1cf9c4d686440598317123469a7c827d4fcdf74715Bob: 0xcf21e85337a313bdac33d068960a3e52d22ce0e6190e9acc03a1c9930e1eaf3eChad: 0xa1a2aef8525eb20655387d3ed50b9a3ea1531ef6117f579d0da4bcf5a2e1f76d
Configuring the Multisig Vault
Section titled “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 accountthreshold = 2multisig_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. TheMultiPublicKey
combines all three public keys into a single multisig configuration. -
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:
Terminal window python3 multisig.pyTerminal window python multisig.pyYou should see output showing your multisig account’s public key type and its unique address on the Aptos blockchain. For example:
Terminal window === 2-of-3 Multisig account ===Account public key: 2-of-3 Multi-Ed25519 public keyAccount address: 0x08cac3b7b7ce4fbc5b18bc039279d7854e4c898cbf82518ac2650b565ad4d364
Funding Our Accounts
Section titled “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_000bob_start = 20_000_000chad_start = 30_000_000multisig_start = 40_000_000# Fund all accounts concurrentlyalice_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 balancesalice_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 useasyncio.gather()
to make all these queries efficiently in parallel. -
Run the script
Verify funding success by running:
Terminal window python3 multisig.pyTerminal window python multisig.pyThe output should show each account with its respective balance. For example:
Terminal window === Funding accounts ===Alice's balance: 10000000Bob's balance: 20000000Chad's balance: 30000000Multisig balance: 40000000
Creating Our First Multisig Transaction
Section titled “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 transactionentry_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 transactionchain_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
- Uses
-
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:
Terminal window python3 multisig.pyTerminal window python multisig.pyYou should see something like:
Terminal window === Individual signatures ===Alice: 0x360e66c75b1ba787ec7b05998cbc14276d7fc0c006fb10c33d5cc3c4cc2ec4f53a8c0996b8e746fd6d86b09b4f8bb128cbf62d8b375f5b974faae040e889ac0dBob: 0xdcfd1965e531deb79de9d8daf7f28f46023107ce4f11612ce76da33e808486a0a368b34563d4f89d6179a3957a266c1e8809691fddabba3c2a3d8be14d6f2f0cThis 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.
Submitting the Multisig Transaction
Section titled “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 configurationauthenticator = 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. TheMultiSignature
andAuthenticator
objects package these signatures in a format the blockchain can verify. -
Submit transaction
Create and submit the signed transaction:
# Create and submit the signed transactionsigned_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 usingsubmit_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:
Terminal window python3 multisig.pyTerminal window python multisig.pyYou should see something like:
Terminal window === Submitting transfer transaction ===Transaction hash: 0x2f0b7fc8e69213f0c7e720e660f789b6e3d3564729a298f2b4f6794245833f2d=== New account balances ===Alice's balance: 10000000Bob's balance: 20000000Chad's balance: 30000100 # Increased by 100 octasMultisig balance: 39999200 # Decreased by 100 octas plus gas feesNotice 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
Going Further: Advanced Features
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Next Steps
Section titled “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.