diff --git a/public/images/pages/billboard.svg b/public/images/pages/billboard.svg
new file mode 100644
index 00000000..f1d4e4a9
--- /dev/null
+++ b/public/images/pages/billboard.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/common/navigation.yaml b/src/common/navigation.yaml
index 8fb2abd8..3d4e1e73 100644
--- a/src/common/navigation.yaml
+++ b/src/common/navigation.yaml
@@ -42,6 +42,7 @@ sections:
pages:
- path: /hello-world-tutorial
- path: /counter-tutorial
+ - path: /billboard-tutorial
- path: /testing-contracts
- path: /build-apps
pages:
diff --git a/src/pages/write-smart-contracts/billboard-tutorial.md b/src/pages/write-smart-contracts/billboard-tutorial.md
new file mode 100644
index 00000000..305f285e
--- /dev/null
+++ b/src/pages/write-smart-contracts/billboard-tutorial.md
@@ -0,0 +1,281 @@
+---
+title: Billboard
+description: Learn how to store data on-chain and transfer STX tokens with Clarity
+duration: 30 minutes
+experience: intermediate
+tags:
+ - tutorial
+images:
+ large: /images/pages/billboard.svg
+---
+
+## Introduction
+
+This tutorial demonstrates how to transfer STX tokens and handle errors in Clarity by building a simple on-chain message
+store. Additionally, this tutorial provides a simple overview of testing a smart contract. This tutorial builds on
+concepts introduced in the [counter tutorial][], and uses [Clarinet][] to develop and test the smart contract.
+
+In this tutorial you will:
+
+- Set up a development environment with Clarinet
+- Define codes for error handling
+- Add a data storage variable with functions to get and set the variable
+- Add a STX transfer function within the variable setter
+- Develop a unit test to verify the contract works as expected
+
+The [final code for this tutorial][] is available in the Clarinet repository.
+
+## Prerequisites
+
+For this tutorial, you should have a local installation of [Clarinet][]. Refer to [Installing Clarinet][] for
+instructions on how to set up your local environment. You should also have a text editor or IDE to edit the Clarity
+smart contract.
+
+For developing the unit test, it's recommended that you have an IDE with Typescript support, such as
+[Visual Studio Code][].
+
+## Step 1: set up the project
+
+With Clarinet installed locally, open a new terminal window and create a new Clarinet project. Add a smart contract and
+an empty test file to the project:
+
+```sh
+clarinet new billboard-clarity && cd billboard-clarity
+clarinet contract new billboard
+```
+
+These commands create the necessary project structure and contracts for completing this tutorial. Remember that at
+any point during this tutorial you can use `clarinet check` to check the validity of your Clarity syntax.
+
+## Step 2: create message storage
+
+Open the `contracts/billboard.clar` file in a text editor or IDE. For this tutorial, you'll use the boilerplate comments
+to structure your contract for easy readability.
+
+In this step, you'll add a variable to the contract that stores the billboard message, and define a getter function to
+read the value of the variable.
+
+Under the `data maps and vars` comment, define the `billboard-message` variable. Remember that you must define the type of
+the variable, in this case `string-utf8` to support emojis and extended characters. You must also define the
+maximum length of the variable, for this tutorial use the value `500` to allow for a longer message. You must also
+define the initial value for the variable.
+
+```clarity
+;; data maps and vars
+(define-data-var billboard-message (string-utf8 500) u"Hello world!")
+```
+
+You also should define a read-only getter function returns the value of the `billboard-message` variable.
+
+```clarity
+;; public functions
+(define-read-only (get-message)
+ (var-get billboard-message))
+```
+
+These are the required methods for storing and accessing the message on the billboard.
+
+## Step 3: define set message function
+
+Define a method to set the billboard message. Under the public functions, define a `set-message` function. This public
+function takes a `string-utf8` with a max length of `500` as the only argument. Note that the type of the argument
+matches the type of the `billboard-message` variable. Clarity's type checking ensures that an invalid input to the
+function doesn't execute.
+
+```clarity
+(define-public (set-message (message (string-utf8 500)))
+ (var-set billboard-message message)
+)
+```
+
+The contract is now capable of updating the `billboard-message`.
+
+## Step 4: transfer STX to set message
+
+In this step, you'll modify the `set-message` function to add a cost in STX tokens, that increments by a set amount each
+time the message updates.
+
+First, you should define a variable to track the price of updating the billboard. This value is in micro-STX. Under the
+`data maps and vars` heading, add a new variable `price` with type `uint` and an initial value of `u100`. The initial
+cost to update the billboard is 100 micro-STX or 0.0001 STX.
+
+```clarity
+(define-data-var price uint u100)
+```
+
+You also should define a read-only getter function returns the value of the `price` variable.
+
+```clarity
+(define-read-only (get-price)
+ (var-get price)
+)
+```
+
+It's a best practice to define codes to a descriptive constant for Clarity smart contracts. This makes the code easier
+to understand for readers. Under the `constants` comment, define a STX transfer error constant. Assign the value `u0` to
+the constant. There is no standard for error constants in Clarity, this value is used because it's the first error the
+contract defines.
+
+```clarity
+(define-constant ERR_STX_TRANSFER u0)
+```
+
+Modify the `set-message` function to transfer the amount of STX represented by the current price of the billboard from
+the function caller to the contract wallet address, and then increment the new price. The function is then executed in four steps: transferring STX from the function caller to the contract, updating the `billboard-message` variable, incrementing the
+`price` variable, and returning the new price.
+
+The new `set-message` function uses [`let`][] to define local variables for the function. Two variables are declared,
+the `cur-price`, which represents the current price of updating the billboard, and the `new-price`, which represents the
+incremented price for updating the billboard.
+
+The function then calls the [`stx-transfer?`][] function to transfer the current price of the contract in STX from the
+transaction sender to the contract wallet. This syntax can be confusing: the function call uses the `tx-sender`
+variable, which is the principal address of the caller of the function. The second argument to [`stx-transfer?`][] uses
+the [`as-contract`][] function to change the context's `tx-sender` value to the principal address that deployed the
+contract.
+
+The entire [`stx-transfer?`][] function call is wrapped in the [`unwrap!`][] function, to provide protection from
+the transfer failing. The [`unwrap!`][] function executes the first argument, in this case the [`stx-transfer?`][]
+function. If the execution returns `(ok ...)`, the [`unwrap!`][] function returns the inner value of the `ok`, otherwise
+the function returns the second argument and exits the current control-flow, in this case the `ERR_STX_TRANSFER` error
+code.
+
+If the token transfer is successful, the function sets the new `billboard-message` and updates the `price` variable to
+`new-price`. Finally, the function returns `(ok new-price)`. It's generally a good practice to have public functions
+return `ok` when successfully executed.
+
+```clarity
+(define-public (set-message (message (string-utf8 500)))
+ (let ((cur-price (var-get price))
+ (new-price (+ cur-price u10)))
+
+ ;; pay the contract
+ (unwrap! (stx-transfer? cur-price tx-sender (as-contract tx-sender)) (err ERR_STX_TRANSFER))
+
+ ;; update the billboard's message
+ (var-set billboard-message message)
+
+ ;; update the price
+ (var-set price new-price)
+
+ ;; return the updated price
+ (ok new-price)
+ )
+)
+```
+
+At this point, the final contract should look like this:
+
+```clarity
+;; error consts
+(define-constant ERR_STX_TRANSFER u0)
+
+;; data maps/vars
+(define-data-var billboard-message (string-utf8 500) u"Hello World!")
+(define-data-var price uint u100)
+
+;; public functions
+(define-read-only (get-price)
+ (var-get price)
+)
+
+(define-read-only (get-message)
+ (var-get billboard-message)
+)
+
+(define-public (set-message (message (string-utf8 500)))
+ (let ((cur-price (var-get price))
+ (new-price (+ cur-price u10)))
+
+ ;; pay the contract
+ (unwrap! (stx-transfer? cur-price tx-sender (as-contract tx-sender)) (err ERR_STX_TRANSFER))
+
+ ;; update the billboard's message
+ (var-set billboard-message message)
+
+ ;; update the price
+ (var-set price new-price)
+
+ ;; return the updated price
+ (ok new-price)
+ )
+)
+```
+
+## Step 5: write a contract test
+
+At this point, the contract functions as intended, and can be deployed to the blockchain. However, it's good practice
+to write automated testing to ensure that the contract functions perform in the expected way. Testing can be valuable
+when adding complexity or new functions, as working tests can verify that any changes you make didn't fundamentally
+alter the way the functions behave.
+
+Open the `tests/billboard_test.ts` file in your IDE. In this step, you will add a single automated test to exercise the
+`set-message` and `get-message` functions of the contract.
+
+```ts
+import { Clarinet, Tx, Chain, Account, types } from 'https://deno.land/x/clarinet@v0.10.0/index.ts';
+import { assertEquals } from 'https://deno.land/std@0.90.0/testing/asserts.ts';
+
+Clarinet.test({
+ name: 'Ensure that the message can be set',
+ async fn(chain: Chain, accounts: Map) {
+ let wallet_1 = accounts.get('wallet_1')!;
+
+ let assetMaps = chain.getAssetsMaps();
+ const balance = assetMaps.assets['STX'][wallet_1.address];
+
+ let block = chain.mineBlock([
+ Tx.contractCall('billboard', 'set-message', [types.utf8('testing')], wallet_1.address),
+ Tx.contractCall('billboard', 'get-message', [], wallet_1.address),
+ Tx.contractCall('billboard', 'set-message', [types.utf8('testing...')], wallet_1.address),
+ Tx.contractCall('billboard', 'get-message', [], wallet_1.address),
+ ]);
+
+ assertEquals(block.receipts.length, 4);
+ assertEquals(block.height, 2);
+
+ block.receipts[1].result.expectUtf8('testing');
+
+ block.receipts[3].result.expectUtf8('testing...');
+
+ assetMaps = chain.getAssetsMaps();
+ assertEquals(assetMaps.assets['STX'][wallet_1.address], balance - 210);
+ },
+});
+```
+
+Modify the default imports in the boilerplate to include `{ Clarinet, Tx, Chain, Account, types }` from the `clarinet`
+Deno library.
+
+Give the test a descriptive name using the `name` field, then modify the test function to include the `chain` and
+`accounts` arguments. Make sure that you define a type for each of the arguments to satisfy the Typescript compiler.
+
+Using the Clarinet library, define variables to get a wallet address principal from the Clarinet configuration, and the
+balance of that address on the chain.
+
+The functional part of the test is defined using the `chain.mineBlock()` function, which simulates the mining of a
+block. Within that function, the test makes 4 contract calls (`Tx.contractCall()`), 2 calls to `set-message` and 2 calls
+to `get-message`.
+
+Once the simulated block is mined, the test can make assertions about the chain state. This is accomplished using the
+`assertEquals()` function and the `expect` function. In this case, the test asserts that the once the simulated block
+is mined, the block height is now equal to `2`, and that the number of receipts (contract calls) in the block are
+exactly `4`.
+
+The test can then make assertions about the return values of the contract. The test checks that the result of the
+transaction calls to `get-message` match the string values that the calls to `set-message` contain. This covers the
+capability of both contract functions. Finally, the test asserts that STX were transferred from the transaction
+caller wallet, covering the price updating and token transfer.
+
+-> You have now learned how to store and update data on chain with a variable, and how to transfer STX tokens from
+a contract caller to a new principal address.
+
+[counter tutorial]: /write-smart-contracts/counter-tutorial
+[clarinet]: /write-smart-contracts/clarinet
+[installing clarinet]: /write-smart-contracts/clarinet#installing-clarinet
+[visual studio code]: https://code.visualstudio.com/
+[final code for this tutorial]: https://github.com/hirosystems/clarinet/tree/master/examples/billboard
+[`let`]: /references/language-functions#let
+[`stx-transfer?`]: /references/language-functions#stx-transfer
+[`as-contract`]: /references/language-functions#as-contract
+[`unwrap!`]: /references/language-functions#unwrap
diff --git a/src/pages/write-smart-contracts/overview.md b/src/pages/write-smart-contracts/overview.md
index 3212279a..329d125c 100644
--- a/src/pages/write-smart-contracts/overview.md
+++ b/src/pages/write-smart-contracts/overview.md
@@ -69,7 +69,7 @@ Note some of the key Clarity language rules and limitations.
## Try a tutorial
[@page-reference | grid]
-| /write-smart-contracts/hello-world-tutorial, /write-smart-contracts/counter-tutorial, /build-apps/guides/transaction-signing, /build-apps/tutorials/public-registry
+| /write-smart-contracts/hello-world-tutorial, /write-smart-contracts/counter-tutorial, /write-smart-contracts/billboard-tutorial
## Explore more