Overview
Learn how post-conditions protect users from unexpected transaction outcomes.

The Big Picture
Post-conditions are constraints you attach to a transaction that define exactly what assets (STX, SIP-010 tokens, NFTs) are allowed to move and how much. Starting with SIP-045, they also define what staking and PoX actions are allowed to occur.
If the underlying smart contract execution would violate your declared limits, the entire transaction aborts.
Even if a smart contract contains unexpected logic, it cannot move assets beyond what your post-conditions permit.
Post-conditions are constructed on the client-side usually by the client-side developer. They are part of the signed transaction. Contracts cannot modify them.
Wallets will parse the signed transaction and display the declared post-conditions to the user before broadcasting.
What are post-conditions?
Post-conditions are assertions about an on-chain transaction that must be met; otherwise, the transaction will abort during execution. Post-conditions act as a safety net, allowing you to specify what state changes can occur in a transaction.
At times, the client-side developer is not the same person who wrote the underlying smart contract and may not be deeply familiar with its internal logic, nested external contract calls, or edge cases. This logic helps limit the amount of damage that can be done to a user and their assets, whether due to a bug or malicious behavior.
Post conditions are a set of conditions that must be met before a user's transaction will execute. The primary goal behind post conditions is to limit the amount of damage that can be done to a user's assets due to a bug, intentional or otherwise.
Post conditions are an additional safety feature built into the Stacks protocol itself that help to protect end users. Rather than being a feature of Clarity smart contracts, they are implemented on the client side and meant to be an additional failsafe against malicious contracts.
They are sent as part of the transaction when the user initiates it, meaning we need to implement post-conditions on the frontend. Whenever you are transferring an asset (fungible or non-fungible) from one address to another, you should take advantage of post conditions.
Concrete Example
Frontend dev implements this donation script
A frontend dev is building a frontend app to allow users to donate STX by calling a particular Clarity contract. This Clarity contract has not been audited, for example. So out of caution, the frontend dev will declare post-condition statements to be passed into the construction of the transaction payload.
Remember, we are assuming the frontend developer is not the same person who wrote the underlying smart contract and may not be deeply familiar with its internal logic, nested external contract calls, or edge cases.
User sees this on the frontend
On the left side of the image, the user sees an input field to specify how much in STX they want to donate. The 'Donate' button allows them to construct the transaction and prompt their wallet to popup a confirmation modal.
On the right side of the image, wallets parse the passed transaction payload and displays the declared post-condition statements to the user before broadcasting. This allows the user to know what asset transfers to expect will occur during the execution of the transaction.
The user specified a donation amount of 5 STX, and the user should expect to only transfer exactly 5 STX during the transaction execution.

What the contract function donate looks like
This is what the actual contract function of donate looks like. We are assuming that the frontend developer and the user is blindly aware of what this contract is actually doing.
We can see that the contract will maliciously ignore the amount argument and instead transfer 100 STX out of the sender's wallet.
End result
The Stacks protocol will evaluate the proposed runtime result of the transaction with the declared post-conditions and determine if they match. In this example, the user expected to transfer out exactly 5 STX, but the contract wanted to transfer 100 STX. This discrepancy is evaluated to result in the transaction aborting and failing.

The post-condition stack
Post-conditions are enforced by the Stacks protocol itself but do not exist in the smart contracts themselves. They are programmatically constructed in your front-end application code using Stacks.js, specifically by passing them in as options to the transaction payload construction.
By having post-conditions in the frontend code, Stacks-enabled wallets, such as Leather and Xverse, are able to display the post-conditions in a human-readable format for the user when confirming their transactions. Once a user confirms the transaction, the post-conditions get carried along with the transaction payload where eventually the Stacks protocol will evaluate them together.

If there were no post-conditions in the front-end application code, a user's wallet will display an abstract warning message, where it would be up to the user to decide whether they want to blindly proceed with the transaction or not. And whatever the underlying contract code wants to do, it will do without any post-condition restrictions. So if a contract tries to send your STX tokens to a drainer wallet, it will without you knowing.
Even with post-conditions set up on the frontend code, a user is still blind to the underlying Clarity smart contract code, but at least they know what to expect will happen in the transaction.
Using the Pc helper
The Pc helper provides a chainable API for creating post-condition statements with better type safety and readability.
The builders above are defined in pc.ts and the objects they return in postcondition-types.ts. Both links are pinned to the commit that introduced the staking and PoX conditions, so they will keep pointing at the code these examples were written against.
Staking and PoX post-conditions come from SIP-045 and are available starting with Stacks epoch 4.0 (Bitcoin Staking), in @stacks/transactions 7.5.0 and later. See the Implementation page for details.
These two conditions moved from SIP-044 to SIP-045 late in the SIP process. The @stacks/transactions source still labels them SIP-044 in its JSDoc; SIP-045 is the correct reference.
Manual creation
Create post-conditions manually using type definitions when building conditions dynamically.
Available condition types:
eq: Exactly equal to amountgt: Greater than amountgte: Greater than or equal to amountlt: Less than amountlte: Less than or equal to amount
Fungible tokens
Non-fungible tokens
Available non-fungible condition types:
sent: The NFT MUST have been sent by the principalnot-sent: The NFT MUST NOT have been sent by the principalmaybe-sent: The NFT may or may not have been sent by the principal
maybe-sent was added by SIP-040 and is available starting with Stacks epoch 3.4, in @stacks/transactions 7.4.0 and later. It fills the same gap for NFTs that lte fills for fungible amounts: declaring that a transfer is permitted without requiring it. The condition always passes, and it still counts as covering that specific NFT instance when deny or originator mode checks whether every asset movement was accounted for, so you can authorise an optional NFT transfer without falling back to allow mode.
Staking (SIP-045)
Guards staking STX (or modifying staked STX) for a principal. Uses the same comparators as the STX post-condition; amounts are denoted in uSTX.
PoX (SIP-045)
Guards PoX state changes that do not alter locking status. Carries only a principal and one of three condition codes: no asset or amount.
Post-Condition Modes
Control how unspecified asset transfers are handled with post-condition mode.

Deny Mode
Deny mode is the default for post-conditions. Deny is a more-strict setting for post-conditions, and it says that any other asset transfers that do not meet the criteria of the post-condition are denied. In deny mode, any transaction that does not meet the post-condition criteria will fail.
This setting is useful when you want to limit any transfer events to a specific set of criteria. It is why deny is the default if you don't or forget to pass in a postConditionMode option.
Post-condition mode
Always use Deny mode unless you have a specific reason to allow additional transfers.
Allow mode
Allow mode is a less-strict setting, in which it "allows" any transaction to execute as long as it meets the criteria of the specified post-conditions. This allow mode enables additional transactions to occur as long as the post-condition is met in that process. This setting is useful when you want to allow other unknown or dynamic transfers to happen. But usually you wouldn't want to have this happen as this can open up unintended consequences for the user.
Originator mode (SIP-040)
Originator mode is a hybrid of the two: it applies deny-style protection to the transaction's origin account while allowing asset movements between other principals. In originator mode, no asset transfers from the origin account are permitted besides those named in the post-conditions. Transfers between other principals (contracts routing assets among themselves, for example) proceed unrestricted.
The origin account is the transaction's signer (the first signing account in a sponsored transaction). It stays fixed throughout execution: it is not tx-sender, and it is unaffected by as-contract?.
This mode is designed for DeFi-style contract calls where enumerating every intermediate asset movement in deny mode is impractical, but the user still wants a hard cap on what can leave their own account. Available starting with Stacks epoch 3.4 (SIP-040), in @stacks/transactions 7.4.0 and later.
How post-conditions appear to the user
Since post-conditions are declared on your frontend code, they also need to be visually displayed to users. Stacks-supported wallets handle that by displaying post-conditions on the transaction confirmation modals that popup when a user needs to confirm/approve a transaction.

Things to be aware of
Post-conditions have limitations you should keep in mind. Post-conditions only track who sends an asset, and how much. They do not monitor who owns any set of assets when the transaction finishes, nor do they monitor the sequence of owners an asset might have during transaction execution. The staking and PoX post-conditions added in SIP-045 follow the same principle: they constrain what a specific principal locks or which PoX actions it performs, not the resulting state.
Post-conditions are not a catch-all. Implementing them does not guarantee your contract or your next transaction is safe. Bugs still occur, and you still need to build with security in mind.
Additional Resources
[Hiro Blog] A Developer's Guide to Post-Conditions
[dev.to] Understanding Stacks Post Conditions
[Hiro YT] ELI5: Post-Conditions on Stacks
[Hiro YT] Understanding Post-Conditions in a Stacks Blockchain Transaction
[StacksGov] Post-conditions section in SIP-005
[StacksGov] SIP-040: Originator mode and the MAY SEND NFT condition
[StacksGov] SIP-045: pox-5 Bitcoin staking, including the staking and PoX post-condition framework
[stacks.js]
Pcpost-condition builder source, pinned to the staking/PoX commit
Last updated
Was this helpful?