Build with Python
Alice and Bob have settled on their design and are ready to kick-off development of their Algorand-powered NFT auction dApp! This guide will take you through the steps to build the dApp, which includes setting up a development environment, writing the smart contract code, deploying it, and writing the functions to interact with it.
A few tips before getting started. The goal of this guide is to get you up and running with a working prototype that represents a real use case, as quickly as possible. We use a hands-on example to teach you basic design principles and best practices for building dApps with Python on Algorand. This guide does not cover all the details of the smart contract code. This is intentional so that you can focus on solidifying higher-level concepts that will be the foundation for building any dApp on Algorand. So don’t worry if you don’t understand what everything does in the smart contract. This is expected! After you feel comfortable with the basics, you can head over to the detailed PyTeal documentation and work on becoming an expert in the PyTeal smart contract language.
Let’s get started.
How this is organized¶
This guide is organized into two sections. The first section helps you launch the dApp and run a mock auction. The second section provides an overview of the different components of the auction application.
All of the code for this guide is located here. Clone the repo and follow along!
Launching the application¶
Algorand provides a Docker instance for running a node, which is the fastest way to set up your dApp’s connection point to the blockchain. Make sure you have Docker installed, then run this code to launch a sandbox node with a private network, i.e. your own local blockchain that runs the Algorand protocol.

Prerequisites¶
Clone the auction demo application¶
Clone the repository using the following commands.
git clone https://github.com/algorand/auction-demo
cd auction-demo
Install Sandbox¶
Run the following script directly from your terminal if you are on Mac or Linux. If you are on Windows, run from a terminal that supports bash, such as Git Bash. This command will clone down the sandbox repository to ./_sandbox
and start some docker containers.
./sandbox up
Watch Video
More Information
Follow Sandbox tutorial
Note
The above command will default to the release
configuration. Other configurations that are compatible with this demo are beta
, nightly
and dev
. This is because the app requires an unencrypted-default-wallet to sign transactions.
If the sandbox fails to start please follow setup directions here.
Setup environment and run tests¶
Install the required dependencies for the project. We recommend using a Python virtual environment to do this. The following commands will activate the virtual environment and then install all dependencies, including PyTeal and the Algorand Python SDK.
Setup Python environment (one time).
python3 -m venv venv
Activate venv
. Replace bin
with Scripts
on Windows.
. venv/bin/activate
. venv/bin/activate.fish
Now install all requirements.
pip3 install -r requirements.txt
Run the example.py
file. This will run a simple 30-second auction.
python3 example.py
#output
Generating temporary accounts...
Alice is generating an example NFT...
The NFT ID is 17
Alice's balances: {0: 99999000, 17: 1}
Bob is creating an auction that lasts 30 seconds to auction off the NFT...
Done. The auction app ID is 18 and the escrow account is...
Alice is setting up and funding NFT auction...
Alice's balances: {0: 99998000, 17: 0}
Auction escrow balances: {0: 202000, 17: 1}
Carla wants to bid on NFT, her balances: {0: 100000000}
Carla is placing bid for 1000000 microAlgos
Carla is opting into NFT with ID 17
Waiting 16 seconds for the auction to finish
Alice is closing out the auction
The auction escrow now holds the following: {0: 0}
Alice's balances after auction: {0: 101197100, 17: 0} Algos
Carla's balances after auction: {0: 98997098, 17: 1} Algos
Optionally Run tests.
pytest
This will run through all the tests in the operations_test.py
file and will take a little over six minutes to execute.
Congrats! You’re already up and running.
Whenever you're finished testing, you can use the following command to shutdown sandbox.
./sandbox down
Before we dig into the code, consider checking out Dappflow to take a look at all the transactions that have been created from the above tests. Dappflow is a Block Explorer for use with public and private Algorand networks. You are able to view blocks, transactions, accounts, assets, and applications.
Application overview¶
The auction demo application uses a smart contract to auction off a non-fungible token. The smart contract covers four basic methods that are used to achieve this functionality.
The first method creates the smart contract and initializes the auction state on the blockchain, which includes the auction creator’s account, the specific NFT ID to auction, the NFT seller’s account, the start and end times of the auction, the reserve amount for the auction, and the minimum bid increment.

The second method completes the auction set up by funding the smart contract with a small amount of algos (to fulfill minimum balance requirements and to pay transaction fees) and by moving the NFT into the smart contract.

The third method constructs the bid scenario. This involves the potential buyer sending the bid in algos to the smart contract. If the bid is successful, the contract will hold the algos. Bidding is only allowed between the beginning and end times of the auction. If the bid supplants a previous bid, the contract automatically refunds the previous higher bidder’s algos.

The fourth and final method of the contract allows the auction to be closed out, which will either award the NFT to the highest bidder and transfer the algos to the seller or return the NFT to the seller and close out any remaining algos to the seller.

Smart contracts live on the blockchain and are remotely callable by a special transaction type called an application transaction. The simplest way to develop smart contracts is to use the PyTeal library with the Algorand Python SDK. Use PyTeal to write the smart contract logic that will be stored (the on-chain value transfer logic). Use the SDK to deploy the smart contract and then to create the application transactions that will interact with the smart contract (the off-chain logic and smart contract triggers).
The auction demo code separates the smart contract code and the SDK operations into two separate files. contracts.py
contains all the logic for the smart contract and operations.py
contains the SDK code to deploy the smart contract and communicate with it once deployed. These two files represent the bulk of the entire application.
Let’s first take a look at the smart contract code.
The smart contract¶
The contracts.py
file contains the smart contract code and is written with the PyTeal library. In this example, a new smart contract is deployed for each new auction. And as we described in the overview, every auction needs to support creating, setting up, placing a bid, and closing out an auction. In a bit, we’ll take you through where each of these scenarios is represented in the code.
Smart contracts on Algorand are actually composed of two separate “programs”. The first is the approval program and the second is the clear program. You’ll see that most of the code lives in the approval program, so we don’t worry too much about the clear program right now. We’ll cover it in detail in the smart contract details section.
Let’s move on to the top-level routing code for the approval program of the smart contract.
program = Cond(
[Txn.application_id() == Int(0), on_create],
[Txn.on_completion() == OnComplete.NoOp, on_call],
[
Txn.on_completion() == OnComplete.DeleteApplication,
on_delete,
],
[
Or(
Txn.on_completion() == OnComplete.OptIn,
Txn.on_completion() == OnComplete.CloseOut,
Txn.on_completion() == OnComplete.UpdateApplication,
),
Reject(),
],
)
This code handles each type of application transaction that could be sent to the approval program. The OnComplete
values distinguish these types. However, you may have noticed that the first line stands out from the rest. When a smart contract is first created, its application ID will always equal zero. The first line utilizes this fact to route to the auction creation logic, assigned to the on_create
variable.
Let’s visit the creation code next.
Create auction¶
on_create_start_time = Btoi(Txn.application_args[2])
on_create_end_time = Btoi(Txn.application_args[3])
on_create = Seq(
App.globalPut(seller_key, Txn.application_args[0]),
App.globalPut(nft_id_key, Btoi(Txn.application_args[1])),
App.globalPut(start_time_key, on_create_start_time),
App.globalPut(end_time_key, on_create_end_time),
App.globalPut(reserve_amount_key, Btoi(Txn.application_args[4])),
App.globalPut(min_bid_increment_key, Btoi(Txn.application_args[5])),
App.globalPut(lead_bid_account_key, Global.zero_address()),
Assert(
And(
Global.latest_timestamp() < on_create_start_time,
on_create_start_time < on_create_end_time
)
),
Approve(),
)
This code takes all the parameters for the auction and stores them in the on-chain global state of the smart contract. It also verifies that the start and end times have not already passed.
The next conditional in our main program routing code is for a NoOp
call. The NoOp
application transaction is just a generic smart contract call that can include arguments, like strings that may trigger further conditions. In this contract, we can supply a NoOp
call with either the “setup” or “bid” argument, which will further route to the appropriate logic for each of those scenarios. This next layer routing is displayed within the on_call
variable.
on_call_method = Txn.application_args[0]
on_call = Cond(
[on_call_method == Bytes("setup"), on_setup],
[on_call_method == Bytes("bid"), on_bid],
)
Setup the auction¶
Let’s follow the “setup” path to the on_setup
code.
on_setup = Seq(
Assert(Global.latest_timestamp() < App.globalGet(start_time_key)),
# opt into NFT asset -- because you can't opt in if you're already opted in, this is what
# we'll use to make sure the contract has been set up
InnerTxnBuilder.Begin(),
InnerTxnBuilder.SetFields(
{
TxnField.type_enum: TxnType.AssetTransfer,
TxnField.xfer_asset: App.globalGet(nft_id_key),
TxnField.asset_receiver: Global.current_application_address(),
}
),
InnerTxnBuilder.Submit(),
Approve(),
)
Bid in the auction¶
Back to the auction! Let’s now check out the on_bid
scenario which will be evaluated if a user supplied “bid” with a NoOp
application transaction.
on_bid_txn_index = Txn.group_index() - Int(1)
on_bid_nft_holding = AssetHolding.balance(
Global.current_application_address(), App.globalGet(nft_id_key)
)
on_bid = Seq(
on_bid_nft_holding,
Assert(
And(
# the auction has been set up
on_bid_nft_holding.hasValue(),
on_bid_nft_holding.value() > Int(0),
# the auction has started
App.globalGet(start_time_key) <= Global.latest_timestamp(),
# the auction has not ended
Global.latest_timestamp() < App.globalGet(end_time_key),
# the actual bid payment is before the app call
Gtxn[on_bid_txn_index].type_enum() == TxnType.Payment,
Gtxn[on_bid_txn_index].sender() == Txn.sender(),
Gtxn[on_bid_txn_index].receiver()
== Global.current_application_address(),
Gtxn[on_bid_txn_index].amount() >= Global.min_txn_fee(),
)
),
If(
Gtxn[on_bid_txn_index].amount()
>= App.globalGet(lead_bid_amount_key) + App.globalGet(min_bid_increment_key)
).Then(
Seq(
If(App.globalGet(lead_bid_account_key) != Global.zero_address()).Then(
repayPreviousLeadBidder(
App.globalGet(lead_bid_account_key),
App.globalGet(lead_bid_amount_key),
)
),
App.globalPut(lead_bid_amount_key, Gtxn[on_bid_txn_index].amount()),
App.globalPut(lead_bid_account_key, Gtxn[on_bid_txn_index].sender()),
App.globalPut(num_bids_key, App.globalGet(num_bids_key) + Int(1)),
Approve(),
)
),
Reject(),
)
Here we check to see if the bid is higher than the current bid leader (stored in the contract state on-chain). If this is a new bidder and the corresponding new bid exceeds the prior one, then the smart contract issues a transaction to refund the previous highest bidder. This code also verifies that the auction is still active and that the bidder submitted a payment transaction (their bid) with the application transaction to the smart contract.
Close out the auction¶
The auction is closed out by using a DeleteApplication
application transaction. This type of transaction will try to delete the smart contract after evaluating the logic in the on_delete
variable.
on_delete = Seq(
If(Global.latest_timestamp() < App.globalGet(start_time_key)).Then(
Seq(
# the auction has not yet started, it's ok to delete
Assert(
Or(
# sender must either be the seller or the auction creator
Txn.sender() == App.globalGet(seller_key),
Txn.sender() == Global.creator_address(),
)
),
# if the auction contract account has opted into the nft, close it out
closeNFTTo(App.globalGet(nft_id_key), App.globalGet(seller_key)),
# if the auction contract still has funds, send them all to the seller
closeAccountTo(App.globalGet(seller_key)),
Approve(),
)
),
If(App.globalGet(end_time_key) <= Global.latest_timestamp()).Then(
Seq(
# the auction has ended, pay out assets
If(App.globalGet(lead_bid_account_key) != Global.zero_address())
.Then(
If(
App.globalGet(lead_bid_amount_key)
>= App.globalGet(reserve_amount_key)
)
.Then(
# the auction was successful: send lead bid account the nft
closeNFTTo(
App.globalGet(nft_id_key),
App.globalGet(lead_bid_account_key),
)
)
.Else(
Seq(
# the auction was not successful because the reserve was not met: return
# the nft to the seller and repay the lead bidder
closeNFTTo(
App.globalGet(nft_id_key), App.globalGet(seller_key)
),
repayPreviousLeadBidder(
App.globalGet(lead_bid_account_key),
App.globalGet(lead_bid_amount_key),
),
)
)
)
.Else(
# the auction was not successful because no bids were placed: return the nft to the seller
closeNFTTo(App.globalGet(nft_id_key), App.globalGet(seller_key))
),
# send remaining funds to the seller
closeAccountTo(App.globalGet(seller_key)),
Approve(),
)
),
Reject(),
)
This code is relatively long because it has to handle several possible close out scenarios. The first is the case where the auction is deleted before it even starts. In this case, the smart contract sends the NFT and all the algos back to the seller. In the case where the auction is completed and the reserve is met, the smart contract will send the NFT to the winning bidder and send all algos to the seller. If the reserve is not met, the smart contract will refund the highest bidder’s algos and send the NFT back to the seller. If the auction has no bids the NFT and algos are refunded to the seller. Any account can close out the auction.
Remember, it’s okay if you don’t understand every detail in this contract now. You can come back to it later once you get the basics down. The full source code for the auction contract is available in the auction repository.
Deploy and communicate with the smart contract¶
We just learned how the smart contract program secures the critical value transfer scenarios that occur in the context of the auction (e.g. sending bids, closing it out). Now let’s learn how to deploy it to the blockchain and communicate with it. The operations.py
file uses Algorand Python SDK to construct the appropriate transactions. It contains a method for each of the four scenarios that the auction application supports. The first scenario is to deploy and create the auction contract and is defined in the createAuctionApp
function.
def createAuctionApp(
client: AlgodClient,
sender: Account,
seller: str,
nftID: int,
startTime: int,
endTime: int,
reserve: int,
minBidIncrement: int,
) -> int:
"""Create a new auction.
Args:
client: An algod client.
sender: The account that will create the auction application.
seller: The address of the seller that currently holds the NFT being
auctioned.
nftID: The ID of the NFT being auctioned.
startTime: A UNIX timestamp representing the start time of the auction.
This must be greater than the current UNIX timestamp.
endTime: A UNIX timestamp representing the end time of the auction. This
must be greater than startTime.
reserve: The reserve amount of the auction. If the auction ends without
a bid that is equal to or greater than this amount, the auction will
fail, meaning the bid amount will be refunded to the lead bidder and
the NFT will return to the seller.
minBidIncrement: The minimum difference required between a new bid and
the current leading bid.
Returns:
The ID of the newly created auction app.
"""
approval, clear = getContracts(client)
globalSchema = transaction.StateSchema(num_uints=7, num_byte_slices=2)
localSchema = transaction.StateSchema(num_uints=0, num_byte_slices=0)
app_args = [
encoding.decode_address(seller),
nftID.to_bytes(8, "big"),
startTime.to_bytes(8, "big"),
endTime.to_bytes(8, "big"),
reserve.to_bytes(8, "big"),
minBidIncrement.to_bytes(8, "big"),
]
txn = transaction.ApplicationCreateTxn(
sender=sender.getAddress(),
on_complete=transaction.OnComplete.NoOpOC,
approval_program=approval,
clear_program=clear,
global_schema=globalSchema,
local_schema=localSchema,
app_args=app_args,
sp=client.suggested_params(),
)
signedTxn = txn.sign(sender.getPrivateKey())
client.send_transaction(signedTxn)
response = waitForTransaction(client, signedTxn.get_txid())
assert response.applicationIndex is not None and response.applicationIndex > 0
return response.applicationIndex
ApplicationCreateTxn
, which passes the compiled version of the smart contract to the blockchain. Once deployed the smart contract will be assigned a unique ID and a unique Algorand address.
Next is the setupAuctionApp
function, which is used to fund the smart contract with a small amount of algos to support fees and the minimum balance requirement for an Algorand account, an application transaction telling the smart contract to set up the auction, and an asset transfer transaction of the NFT to the smart contract.
def setupAuctionApp(
client: AlgodClient,
appID: int,
funder: Account,
nftHolder: Account,
nftID: int,
nftAmount: int,
) -> None:
"""Finish setting up an auction.
This operation funds the app auction escrow account, opts that account into
the NFT, and sends the NFT to the escrow account, all in one atomic
transaction group. The auction must not have started yet.
The escrow account requires a total of 0.203 Algos for funding. See the code
below for a breakdown of this amount.
Args:
client: An algod client.
appID: The app ID of the auction.
funder: The account providing the funding for the escrow account.
nftHolder: The account holding the NFT.
nftID: The NFT ID.
nftAmount: The NFT amount being auctioned. Some NFTs has a total supply
of 1, while others are fractional NFTs with a greater total supply,
so use a value that makes sense for the NFT being auctioned.
"""
appAddr = getAppAddress(appID)
suggestedParams = client.suggested_params()
fundingAmount = (
# min account balance
100_000
# additional min balance to opt into NFT
+ 100_000
# 3 * min txn fee
+ 3 * 1_000
)
fundAppTxn = transaction.PaymentTxn(
sender=funder.getAddress(),
receiver=appAddr,
amt=fundingAmount,
sp=suggestedParams,
)
setupTxn = transaction.ApplicationCallTxn(
sender=funder.getAddress(),
index=appID,
on_complete=transaction.OnComplete.NoOpOC,
app_args=[b"setup"],
foreign_assets=[nftID],
sp=suggestedParams,
)
fundNftTxn = transaction.AssetTransferTxn(
sender=nftHolder.getAddress(),
receiver=appAddr,
index=nftID,
amt=nftAmount,
sp=suggestedParams,
)
transaction.assign_group_id([fundAppTxn, setupTxn, fundNftTxn])
signedFundAppTxn = fundAppTxn.sign(funder.getPrivateKey())
signedSetupTxn = setupTxn.sign(funder.getPrivateKey())
signedFundNftTxn = fundNftTxn.sign(nftHolder.getPrivateKey())
client.send_transactions([signedFundAppTxn, signedSetupTxn, signedFundNftTxn])
waitForTransaction(client, signedFundAppTxn.get_txid())
These three transactions are grouped together and submitted to the blockchain.
The placeBid
function is used to place a bid on a specific NFT using the auction application. This function generates two transactions. A payment transaction from the bidder to the smart contract and an application transaction to the smart contract declaring the bid.
def placeBid(client: AlgodClient, appID: int, bidder: Account, bidAmount: int) -> None:
"""Place a bid on an active auction.
Args:
client: An Algod client.
appID: The app ID of the auction.
bidder: The account providing the bid.
bidAmount: The amount of the bid.
"""
appAddr = getAppAddress(appID)
appGlobalState = getAppGlobalState(client, appID)
nftID = appGlobalState[b"nft_id"]
if any(appGlobalState[b"bid_account"]):
# if "bid_account" is not the zero address
prevBidLeader = encoding.encode_address(appGlobalState[b"bid_account"])
else:
prevBidLeader = None
suggestedParams = client.suggested_params()
payTxn = transaction.PaymentTxn(
sender=bidder.getAddress(),
receiver=appAddr,
amt=bidAmount,
sp=suggestedParams,
)
appCallTxn = transaction.ApplicationCallTxn(
sender=bidder.getAddress(),
index=appID,
on_complete=transaction.OnComplete.NoOpOC,
app_args=[b"bid"],
foreign_assets=[nftID],
# must include the previous lead bidder here to the app can refund that bidder's payment
accounts=[prevBidLeader] if prevBidLeader is not None else [],
sp=suggestedParams,
)
transaction.assign_group_id([payTxn, appCallTxn])
signedPayTxn = payTxn.sign(bidder.getPrivateKey())
signedAppCallTxn = appCallTxn.sign(bidder.getPrivateKey())
client.send_transactions([signedPayTxn, signedAppCallTxn])
waitForTransaction(client, appCallTxn.get_txid())
These two transactions are grouped together. Grouping transactions, also called atomic transfers, is a very powerful feature on Algorand. We won’t get into the details here, but check out the Atomic Transfer section later to learn more.
The closeAuction
function is used to close out the auction. This function can be called by any account.
def closeAuction(client: AlgodClient, appID: int, closer: Account):
"""Close an auction.
This action can only happen before an auction has begun, in which case it is
cancelled, or after an auction has ended.
If called after the auction has ended and the auction was successful, the
NFT is transferred to the winning bidder and the auction proceeds are
transferred to the seller. If the auction was not successful, the NFT and
all funds are transferred to the seller.
Args:
client: An Algod client.
appID: The app ID of the auction.
closer: The account initiating the close transaction. This must be
either the seller or auction creator if you wish to close the
auction before it starts. Otherwise, this can be any account.
"""
appGlobalState = getAppGlobalState(client, appID)
nftID = appGlobalState[b"nft_id"]
accounts: List[str] = [encoding.encode_address(appGlobalState[b"seller"])]
if any(appGlobalState[b"bid_account"]):
# if "bid_account" is not the zero address
accounts.append(encoding.encode_address(appGlobalState[b"bid_account"]))
deleteTxn = transaction.ApplicationDeleteTxn(
sender=closer.getAddress(),
index=appID,
accounts=accounts,
foreign_assets=[nftID],
sp=client.suggested_params(),
)
signedDeleteTxn = deleteTxn.sign(closer.getPrivateKey())
client.send_transaction(signedDeleteTxn)
waitForTransaction(client, signedDeleteTxn.get_txid())
This method issues a delete application transaction using the ApplicationDeleteTxn
function.
The full source for operations.py
is available in the auction repository.
Congratulations! You successfully completed the guide to launch Alice’s auction dApp on Algorand. Now that you know the fundamentals. You can jump further into the details of smart contract development and PyTeal or you can first continue onto the tokenization section where you’ll learn how to create NFTs, FTs, and security tokens.