Skip to main content
Thru programs are executable code that runs on the Thru blockchain VM, handling transactions and managing account state. This guide walks you through building a complete counter program using the C SDK.
While this guide uses C, Thru supports multiple programming languages through different SDKs. The core concepts remain the same across all supported languages.

What We’re Building

In this guide, we’ll create a simple but complete counter program that demonstrates essential Thru development concepts:
  • Account Creation: Creating new accounts to store data
  • State Management: Reading and modifying account data
  • Event Emission: Publishing events when state changes
  • Input Validation: Securing your program against invalid data
The counter program supports two operations:
  1. Create: Initialize a new counter account with value 0
  2. Increment: Increase an existing counter’s value by 1

Prerequisites

  • Thru RPC Developer Kit - Follow the setup guide to install the RPC client and CLI tools
  • Thru C SDK - Download thru-program-sdk-c-{version}.tar.gz from GitHub releases
  • RISC-V Toolchain - Required for compiling programs (installed via the C SDK’s deps.sh script)
  • Basic understanding of C programming

Core Concepts

Before diving into program structure, it’s important to understand the fundamental concepts of Thru program development. Entry Point Function
Every Thru program must define an entry point function with the TSDK_ENTRYPOINT_FN attribute. This function serves as the main execution entry point for your program.
TSDK_ENTRYPOINT_FN void start(uchar const *instruction_data, ulong instruction_data_sz) {
    // Your program logic here
}
The entry point receives:
  • instruction_data: Raw instruction data passed to your Thru program
  • instruction_data_sz: Size of the instruction data in bytes
Program Execution Model
Thru programs don’t return values in the traditional sense. Instead, they either complete successfully using tsdk_return() or terminate with an error using tsdk_revert(). The program execution simply exits when complete.
Use tsdk_return() for successful completion:
tsdk_return(TSDK_SUCCESS);
Use tsdk_revert() to terminate with an error:
tsdk_revert(error_code);
Input Validation
Always validate input data size and format to prevent security vulnerabilities and ensure program reliability. This is critical for preventing buffer overflows and malformed data attacks.
We’ll cover detailed validation patterns when we implement the entry point function. SDK Imports When developing programs using the SDK, include these essential headers:
  • <thru-sdk/c/tn_sdk.h> - Core SDK functions and macros
  • <thru-sdk/c/tn_sdk_syscall.h> - System call functions (optional, for advanced operations)

Setting Up Your Project

Before writing any code, let’s set up the project structure and build configuration.
1

Create Project Directory

Create a directory for your program project:
mkdir -p my-thru-project/examples
cd my-thru-project
2

Create Build Configuration

Create a GNUmakefile in your project root:
GNUmakefile
# Simple makefile for my Thru project

BASEDIR:=$(CURDIR)/build
# Set THRU_C_SDK_DIR to the location Thru SDK install. The default directory
# is already set.
THRU_C_SDK_DIR:=$(HOME)/.thru/sdk/c/
include $(THRU_C_SDK_DIR)/thru_c_program.mk
The GNUmakefile sets up paths to the installed Thru C SDK and includes the program build rules.
3

Create Program Build Configuration

Create a Local.mk file in your examples directory:
examples/Local.mk
# My Thru C SDK Counter Program

$(call make-bin,tn_counter_program_c,tn_counter_program,,-ltn_sdk)
The Local.mk file tells the build system which programs to compile using the make-bin macro, linking against the tn_sdk library.The names tn_counter_program_c (output binary name) and tn_counter_program (source file name) will be used for the files we create later in this guide.
Your project structure should now look like this:
my-thru-project/
├── GNUmakefile
└── examples/
    └── Local.mk

Program Structure

Now that your project is set up, let’s create the program files. A Thru program in C requires a minimum of two files:
  • Header file (.h) - Defines interfaces, data structures, and constants
  • Implementation file (.c) - Contains the program logic and entry point
While two files is the minimum requirement, larger programs may include additional source files, utility modules, or other dependencies as needed.
We’ll create both files in the examples/ directory.

Create the Header File

Create examples/tn_counter_program.h with the following content. This header defines the data structures, error codes, and constants for your counter program:
tn_counter_program.h
#ifndef TN_COUNTER_PROGRAM_H
#define TN_COUNTER_PROGRAM_H

#include <thru-sdk/c/tn_sdk.h>

/* Error codes */
#define TN_COUNTER_ERR_INVALID_INSTRUCTION_DATA_SIZE (0x1000UL)
#define TN_COUNTER_ERR_INVALID_INSTRUCTION_TYPE      (0x1001UL)
#define TN_COUNTER_ERR_ACCOUNT_CREATE_FAILED         (0x1002UL)
#define TN_COUNTER_ERR_ACCOUNT_SET_WRITABLE_FAILED   (0x1003UL)
#define TN_COUNTER_ERR_ACCOUNT_RESIZE_FAILED         (0x1004UL)
#define TN_COUNTER_ERR_ACCOUNT_DATA_ACCESS_FAILED    (0x1005UL)

/* Instruction types */
#define TN_COUNTER_INSTRUCTION_CREATE    (0U)
#define TN_COUNTER_INSTRUCTION_INCREMENT (1U)

/* Create counter instruction arguments */
typedef struct __attribute__((packed)) {
    uint instruction_type;
    ushort account_index;
    uchar counter_program_seed[TN_SEED_SIZE];
    uint proof_size;
    /* proof_data follows dynamically based on proof_size */
} tn_counter_create_args_t;

/* Increment counter instruction arguments */
typedef struct __attribute__((packed)) {
    uint instruction_type;
    ushort account_index;
} tn_counter_increment_args_t;

/* Counter account data structure */
typedef struct __attribute__((packed)) {
    ulong counter_value;
} tn_counter_account_t;

#endif

Key components:
  • Error codes: Define unique error codes for debugging
  • Instruction types: Constants identifying each operation (create = 0, increment = 1)
  • Data structures: Define the binary format for instruction arguments and account storage
  • proof_size field: Indicates the size of the cryptographic state proof for account verification

Create the Implementation File

Create examples/tn_counter_program.c with the following content. This file contains the program logic including instruction handlers and the entry point:
tn_counter_program.c
#include <stddef.h>
#include <thru-sdk/c/tn_sdk.h>
#include <thru-sdk/c/tn_sdk_syscall.h>
#include "tn_counter_program.h"

static void handle_create_counter(uchar const *instruction_data, ulong instruction_data_sz TSDK_PARAM_UNUSED) {
    tn_counter_create_args_t const *args = (tn_counter_create_args_t const *)instruction_data;

    /* Use account index from instruction arguments (index 0 is the fee payer, index 1 is the program) */
    ushort account_idx = args->account_index;

    /* Get proof data pointer (follows the struct) */
    uchar const *proof_data = NULL;
    if (args->proof_size > 0) {
        proof_data = instruction_data + sizeof(tn_counter_create_args_t);
    }

    /* Create the account using seed and proof from instruction data */
    ulong result = tsys_account_create(account_idx, args->counter_program_seed, proof_data, args->proof_size);
    if (result != TSDK_SUCCESS) {
        tsdk_revert(TN_COUNTER_ERR_ACCOUNT_CREATE_FAILED);
    }

    /* Set account as writable so we can modify its data */
    result = tsys_set_account_data_writable(account_idx);
    if (result != TSDK_SUCCESS) {
        tsdk_revert(TN_COUNTER_ERR_ACCOUNT_SET_WRITABLE_FAILED);
    }

    /* Resize account to hold counter data */
    result = tsys_account_resize(account_idx, sizeof(tn_counter_account_t));
    if (result != TSDK_SUCCESS) {
        tsdk_revert(TN_COUNTER_ERR_ACCOUNT_RESIZE_FAILED);
    }

    /* Get account data pointer and initialize counter */
    void* account_data = tsdk_get_account_data_ptr(account_idx);
    if (account_data == NULL) {
        tsdk_revert(TN_COUNTER_ERR_ACCOUNT_DATA_ACCESS_FAILED);
    }

    tn_counter_account_t* counter_account = (tn_counter_account_t*)account_data;
    counter_account->counter_value = 0UL;

    tsdk_return(TSDK_SUCCESS);
}

static void handle_increment_counter(uchar const *instruction_data, ulong instruction_data_sz TSDK_PARAM_UNUSED) {
    tn_counter_increment_args_t const *args = (tn_counter_increment_args_t const *)instruction_data;

    ushort account_idx = args->account_index;

    /* Get account data pointer */
    void* account_data = tsdk_get_account_data_ptr(account_idx);
    if (account_data == NULL) {
        tsdk_revert(TN_COUNTER_ERR_ACCOUNT_DATA_ACCESS_FAILED);
    }

    /* Set account as writable so we can modify the counter value */
    ulong result = tsys_set_account_data_writable(account_idx);
    if (result != TSDK_SUCCESS) {
        tsdk_revert(TN_COUNTER_ERR_ACCOUNT_SET_WRITABLE_FAILED);
    }

    /* Increment the counter */
    tn_counter_account_t* counter_account = (tn_counter_account_t*)account_data;
    counter_account->counter_value++;

    /* Emit increment event - emit just the counter value */
    tsys_emit_event((uchar const *)&counter_account->counter_value, sizeof(ulong));

    tsdk_return(TSDK_SUCCESS);
}

TSDK_ENTRYPOINT_FN void start(uchar const *instruction_data, ulong instruction_data_sz) {
    /* Check minimum size to safely read instruction type */
    if (instruction_data_sz < sizeof(uint)) {
        tsdk_revert(TN_COUNTER_ERR_INVALID_INSTRUCTION_DATA_SIZE);
    }

    uint const *instruction_type = (uint const *)instruction_data;

    switch (*instruction_type) {
        case TN_COUNTER_INSTRUCTION_CREATE:
            /* Check minimum size to safely access struct fields */
            if (instruction_data_sz < sizeof(tn_counter_create_args_t)) {
                tsdk_revert(TN_COUNTER_ERR_INVALID_INSTRUCTION_DATA_SIZE);
            }
            tn_counter_create_args_t const *create_args = (tn_counter_create_args_t const *)instruction_data;
            ulong expected_size = sizeof(tn_counter_create_args_t) + create_args->proof_size;

            /* Check exact size including proof data */
            if (instruction_data_sz != expected_size) {
                tsdk_revert(TN_COUNTER_ERR_INVALID_INSTRUCTION_DATA_SIZE);
            }

            handle_create_counter(instruction_data, instruction_data_sz);
            break;

        case TN_COUNTER_INSTRUCTION_INCREMENT:
            /* Check exact instruction size */
            if (instruction_data_sz != sizeof(tn_counter_increment_args_t)) {
                tsdk_revert(TN_COUNTER_ERR_INVALID_INSTRUCTION_DATA_SIZE);
            }
            handle_increment_counter(instruction_data, instruction_data_sz);
            break;

        default:
            tsdk_revert(TN_COUNTER_ERR_INVALID_INSTRUCTION_TYPE);
    }

    /* Should never reach here */
    tsdk_revert(TN_COUNTER_ERR_INVALID_INSTRUCTION_TYPE);
}

Key components:
  • Handler functions: Separate functions for create and increment operations
  • Entry point: The start function validates input and routes to the appropriate handler
  • Input validation: Comprehensive size checks before accessing memory
  • Error handling: Uses tsdk_revert() to exit with error codes
Your project structure is now complete:
my-thru-project/
├── GNUmakefile
└── examples/
    ├── Local.mk
    ├── tn_counter_program.h
    └── tn_counter_program.c

Building Your Program

With your program files in place, build the program:
1

Build the Program

Run the build from your project root:
make
The build system compiles your Thru program for the Thru VM using the installed RISC-V toolchain, generating a .bin file in the build/thruvm/bin/ directory ready for deployment.
Your program is successfully compiled and ready for deployment to the Thru network.

Deploying Your Program

Great! Your counter program is now compiled and ready. The next step is deploying it to the Thru network, which makes your program available for other users and applications to interact with. Deploy your program to the Thru network using the CLI:
1

Create Program on Network

Upload and create your program using a unique seed name:
thru-cli program create <SEED> <PATH_TO_PROGRAM_BINARY>
For our counter example:
thru-cli program create thru_program ./build/thruvm/bin/tn_counter_program_c.bin
You’ll see output similar to this:
Info: Creating permanent managed program from file: ./build/thruvm/bin/tn_counter_program_c.bin (778 bytes)
Info: User seed: thru_program
Info: Step 1: Uploading program to temporary buffer (seed: thru_program_temporary)
Info: 🔍 Checking for existing upload state...
Info:    ℹ No existing upload found
Info: 🚀 Starting fresh upload
Info: Creating meta and buffer accounts...
Success: Transaction completed: tsBBvaXmtyKDn95pAeo03DEiHlobsAc5t80NoFKqFzEwd_5q2PoGTdXNl9gvNU_PmFsSWy168mFcmRMmhL2VsICB8h
Info: Writing chunk 1/1 (778 bytes) at offset 0
Success: Transaction completed: ts5ubIqevoNhioFKswmUolG-7-umnXZwNhTFLD00XQFu05xhspnZK4A6JpgwHr80kW4-fMHHbG44skZzCzZSb1CCBg
Info: Finalizing upload...
Success: Transaction completed: tsXzyg8MIISD4keoNHaTsxEruagay7Q9vcDs69FyDKpjOgiUgrpr5IZLdfesX9n-si-CoFn0bLMJara156AmsADR0j
Success: ✓ Program uploaded to temporary buffer successfully
Info: Temporary meta account: taOxQq4ms4bF2lxs4gM1tkZS90q2ACVo9xvuw-UTzAOO3X
Info: Temporary buffer account: taqrAKYvM6KxZMxotJzaEUhkNQxum-AGr4OKrgN6uCvMMp
Info: Step 2: Creating managed program from temporary buffer
Info: Fee payer: tazLZyk2wT3WO1-_vgH2iqNi0nayD5z2jfFcK10hgrSpan
Info: Manager program meta account: ta8GX2vn4xeY-hGHgrnARdDhz9q3EtcKMnQYOCAtMOza9o
Info: Manager program account: taj9LZ-hhRGi3iBWxaOsXQpO_Lj-tWioGxY_nleuwbpga4
Info: Creating state proofs for permanent program...
Success: Transaction completed: tsq6iZBJwRmShKrVZXl0TrzdVam6LCQ1GdyXXpMuOmfvfpq9BhhHkqwx1jwj3ud4l1V4hxxxoZR1fd94z2UTlYCSEF
Success: ✓ Managed program created successfully
Info: Step 3: Cleaning up temporary buffer account
Info: Cleaning up program accounts...
Info: Executing cleanup transaction...
Success: Transaction completed: ts12OV8Y2p6c-UVaqctw2exA2GOdro7CuXSr5fqLEKMlEWrrilxj-1YuaGsRbtdMpyln5wMuy456OcLjvZRrxLDCK0
Success: ✓ Temporary buffer account cleaned up successfully
Success: 🎉 Permanent managed program created successfully!
Info: Meta account: ta8GX2vn4xeY-hGHgrnARdDhz9q3EtcKMnQYOCAtMOza9o
Info: Program account: taj9LZ-hhRGi3iBWxaOsXQpO_Lj-tWioGxY_nleuwbpga4
2

Note Your Program Accounts

Save the program account addresses from the output:
  • Program account: taj9LZ-hhRGi3iBWxaOsXQpO_Lj-tWioGxY_nleuwbpga4
When you deploy a program, two accounts are created:
  • Meta account - Stores management metadata (authority, version counter, pause state)
  • Program account - Contains the actual executable program code
The two accounts are cryptographically linked: the meta account is derived from your seed, and the program account is derived from the meta account’s address. You’ll use the program account address when calling your program from client applications.
Your counter program is now deployed and ready to receive create and increment instructions on the Thru network.

Updating Your Program

When you need to modify and redeploy your program:
1

Make Code Changes and Rebuild

After making changes to your program code, rebuild it:
make
Always rebuild your program after making code changes to ensure the binary file contains your latest updates.
2

Upgrade the Deployed Program

Use the same seed to upgrade your existing program:
thru-cli program upgrade <SEED> <PATH_TO_PROGRAM_BINARY>
For our counter example:
thru-cli program upgrade thru_program ./build/thruvm/bin/tn_counter_program_c.bin
You’ll see output similar to this:
Info: Upgrading managed program from file: ./build/thruvm/bin/tn_counter_program_c.bin (778 bytes)
Info: User seed: thru_program
Info: Step 1: Uploading program to temporary buffer (seed: thru_program_upgrade_temporary)
Info: 🔍 Checking for existing upload state...
Info:    ℹ No existing upload found
Info: 🚀 Starting fresh upload
Info: Creating meta and buffer accounts...
Success: Transaction completed: tsVAIY50dLC3vjkBC0y08yeoXcoa3M6Mzb9jG6pscofprvMs6SEvYUNOZuXJb0Xx1_1kPh_wVMpRMnlukmvgduCiAT
Info: Writing chunk 1/1 (778 bytes) at offset 0
Success: Transaction completed: tsN2k0h3HS4G8LW_LYYzAumXLYVOfRA4cXiQHFVVjb__kXKjxAM4BF_dMQasIvXKHT8PlGE673B3nS_os_VnObCR9r
Info: Finalizing upload...
Success: Transaction completed: tso9ORuCIqpr2hZBgtBPC1aKZrFHP8mdbWrCr849R4LcSOLcQmJyR7foDbsgoM6lsrCK8jM_5JtQ5DOrw-t0_CCh5w
Success: ✓ Program uploaded to temporary buffer successfully
Info: Temporary meta account: taiMvWgn2SAj5ZHmMCYaja5VzOu-8MmZBTujAVDLdcXYL3
Info: Temporary buffer account: taLsLf6_wdbPXMklS2Ss3VqJNCHnqRSy1CXzb0ZdNwtTH_
Info: Step 2: Upgrading managed program from temporary buffer
Info: Fee payer: tazLZyk2wT3WO1-_vgH2iqNi0nayD5z2jfFcK10hgrSpan
Info: Manager program meta account: ta8GX2vn4xeY-hGHgrnARdDhz9q3EtcKMnQYOCAtMOza9o
Info: Manager program account: taj9LZ-hhRGi3iBWxaOsXQpO_Lj-tWioGxY_nleuwbpga4
Success: Transaction completed: tse-0ldYMRrs45XaDbDTfSnFIXtuDQdY_pd5kDUyrf0iJkZE1u-l8KQntTgWlNdx-QsAPInqINLfjru7UtDHMQBR1Q
Success: ✓ Managed program upgraded successfully
Info: Step 3: Cleaning up temporary buffer account
Info: Cleaning up program accounts...
Info: Executing cleanup transaction...
Success: Transaction completed: tsfQJTncHpt-_lFf0aH6Wefparw7_59NQEv6dQ3kDLJuLNW9-_6nOC0El1NOgNr7854zGE9yCM4Kw7hbTwvC-OASSH
Success: ✓ Temporary buffer account cleaned up successfully
Success: Program upgrade completed! New program size: 778 bytes
The upgrade process uses the same program accounts but updates the program code. Your program account addresses remain the same, so existing client applications continue to work without changes.

Interacting with Your Deployed Program

Perfect! Your counter program is now deployed and ready to use. But before we can send transactions to it, we need to understand how to properly format instructions and organize accounts.

Understanding Account Indexing

When your program executes, it receives an account array with accounts organized in a specific order. Understanding this structure is essential for properly constructing transactions and accessing the right accounts in your program. Account Array Structure: The transaction system organizes accounts in the following order:
  1. Fee Payer Account - The account paying transaction fees (always at index 0)
  2. Program Account - Your deployed program (always at index 1)
  3. Writable Accounts - Accounts the program can modify (sorted in ascending order by hex key)
  4. Read-only Accounts - Accounts the program can only read (sorted in ascending order by hex key)
CLI Transaction Parameters: When using thru-cli txn execute <PROGRAM> <INSTRUCTIONS>:
  • <PROGRAM> - Program account address (becomes index 1 in the account array)
  • <INSTRUCTIONS> - Hex-encoded instruction data (contains account_index specifying which account slot to use)
How Account Creation Works: When you call create counter with a seed, the program uses tsys_account_create() to create a new account. The seed determines the account address - the same seed will always generate the same account address. This means you can:
  1. Use a unique seed to create a counter account
  2. Calculate the resulting account address from the seed
  3. Use that account address in increment instructions
The account address is deterministically derived from the seed, so you don’t need the program to “return” it - you can calculate it independently.

Interacting with Your Program

Once your counter program is deployed, you can send transactions to interact with it. This involves constructing the proper instruction data and sending it to your program account.

Understanding Instruction Data Format

To interact with your counter program, you need to understand how to format the instruction data. Let’s examine the instruction structures defined in your program:
/* Create counter instruction arguments */
typedef struct {
    uint instruction_type;
    ushort account_index;
    uchar counter_program_seed[TN_SEED_SIZE];
} tn_counter_create_args_t;
Key Data Types:
  • uint - 32-bit unsigned integer (4 bytes)
  • ushort - 16-bit unsigned integer (2 bytes)
  • uchar counter_program_seed[TN_SEED_SIZE] - 32-byte seed for deterministic account creation
Instruction Types:
  • Create Counter: instruction_type = 0 + account_index + counter_program_seed[32] + proof_size + proof_data[proof_size]
  • Increment Counter: instruction_type = 1 + account_index
The create instruction is more complex because it includes a variable-length proof that follows the fixed-size struct. The proof data is not part of the struct definition but follows it dynamically based on the proof_size field.

Constructing Instruction Data

To interact with your deployed counter program, you need to construct properly formatted instruction data. This involves three main steps:
  1. Derive the account address where your counter will be stored
  2. Generate a state proof to verify account creation
  3. Encode the instruction data in the format your program expects
The process below shows how to create a new counter. Once created, incrementing is much simpler (just instruction type + account index).
1

Derive Account Address

First, derive the account address where your counter will be stored using the program address and a seed:
thru-cli program derive-address taj9LZ-hhRGi3iBWxaOsXQpO_Lj-tWioGxY_nleuwbpga4 count_acc
Output:
Program ID: taj9LZ-hhRGi3iBWxaOsXQpO_Lj-tWioGxY_nleuwbpga4
Seed: count_acc
Ephemeral: false
Derived Address: tagEPj9hCW8Z9ehMoyJ4UR7faJggQhXb56_8kGxLeitJ1R
The derived address is deterministic - the same program and seed will always generate the same account address.
2

Create State Proof

Generate a state proof for the account you want to create:
thru-cli txn make-state-proof creating tagEPj9hCW8Z9ehMoyJ4UR7faJggQhXb56_8kGxLeitJ1R
Output:
Success: State proof created successfully
Account: tagEPj9hCW8Z9ehMoyJ4UR7faJggQhXb56_8kGxLeitJ1R
Proof Type: creating
Proof Size: 104 bytes
Proof Data (hex): 3502000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
State proofs verify that an account exists or doesn’t exist in the blockchain state tree. For account creation, we use a “creating” proof.
3

Construct Create Counter Instruction Data

Now construct the instruction data by encoding each field according to the struct definition. Recall our create instruction structure:
typedef struct __attribute__((packed)) {
    uint instruction_type;        // 4 bytes
    ushort account_index;         // 2 bytes
    uchar counter_program_seed[TN_SEED_SIZE]; // 32 bytes
    uint proof_size;              // 4 bytes
    /* proof_data follows dynamically based on proof_size */
} tn_counter_create_args_t;
The __attribute__((packed)) attribute is crucial - it tells the compiler to remove padding between struct members, allowing us to concatenate the hex values directly without considering alignment bytes.
For Production Use: While we show manual hex construction here for educational purposes, you’d typically use a client library or SDK to handle this encoding automatically. This detailed breakdown helps you understand what happens under the hood.
1. Instruction Type (CREATE = 0): uint in little endian
Value: 0 → 00000000
2. Account Index (2): ushort in little endian
Value: 2 → 0200
Account index 2 refers to the first writable account in the transaction (index 0 = fee payer, index 1 = program, index 2+ = other accounts).
3. Counter Program Seed: 32-byte array, zero-padded
Seed: "count_acc" (9 bytes)
Hex: 636f756e745f616363
Padded to 32 bytes: 636f756e745f6163630000000000000000000000000000000000000000000000
4. Proof Size (104 bytes): uint in little endian
Value: 104 → 68000000
The proof_size field indicates the size of the cryptographic proof needed to verify the account in the state tree. If proof_size = 0, no proof is needed.
5. Proof Data: 104 bytes from state proof
3502000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Complete Instruction Data: Concatenate all components
00000000 + 0200 + 636f756e745f6163630000000000000000000000000000000000000000000000 + 68000000 + 3502000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Final hex string:
000000000200636F756E745F6163630000000000000000000000000000000000000000000000680000003502000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
4

Increment Counter Instruction

For incrementing an existing counter, the instruction is much simpler as it only needs the instruction type and account index:
typedef struct __attribute__((packed)) {
    uint instruction_type;        // 4 bytes
    ushort account_index;         // 2 bytes
} tn_counter_increment_args_t;
1. Instruction Type (INCREMENT = 1): uint in little endian
Value: 1 → 01000000
2. Account Index (2): ushort in little endian
Value: 2 → 0200
Complete Instruction Data:
010000000200

Sending Transactions

Once you have constructed your instruction data, you can send transactions to interact with your deployed counter program.
1

Execute Create Counter Transaction

Send the create counter transaction using the CLI:
thru-cli txn execute \
  --fee 0 \
  --readwrite-accounts tagEPj9hCW8Z9ehMoyJ4UR7faJggQhXb56_8kGxLeitJ1R \
  taj9LZ-hhRGi3iBWxaOsXQpO_Lj-tWioGxY_nleuwbpga4 \
  000000000200636F756E745F6163630000000000000000000000000000000000000000000000680000003502000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Important: The --readwrite-accounts parameter uses tagEPj9hCW8Z9ehMoyJ4UR7faJggQhXb56_8kGxLeitJ1R - this is the program-derived address we computed earlier using the seed “count_acc”. This is the address where our counter account will be created and stored.
Output:
Success: Transaction executed successfully
Signature: tsWCCCBvTAx4pMkYbeW8hMrf6cPax6ujQ7o_uGV378qS_lm25sN2tbMXvfrL-5ouYObwCMYpgvi1AEumc_fFeRAh-6
Slot: 567
Compute Units Consumed: 7524
State Units Consumed: 1
Execution Result: 0
VM Error: 0
User Error Code: 0
Events Count: 0
Events Size: 0
Pages Used: 2
Command Structure:
  • thru-cli txn execute - Base command for transaction execution
  • --fee 0 - Set transaction fee to 0 for testing
  • --readwrite-accounts <account> - The program-derived address where the counter will be created/stored
  • <program_address> - Your deployed program’s account address
  • <instruction_data> - The hex-encoded instruction data we constructed
The account ordering follows the transaction specification: fee payer (index 0), program (index 1), then writable accounts (index 2+). Our counter account will be at index 2. The --readwrite-accounts parameter tells the transaction system which accounts the program is allowed to modify - in this case, the counter account we’re about to create. Note that State Units Consumed: 1 confirms the account was successfully created.
2

Execute Increment Counter Transaction

After successfully creating a counter, you can increment it:
thru-cli txn execute \
  --fee 0 \
  --readwrite-accounts tagEPj9hCW8Z9ehMoyJ4UR7faJggQhXb56_8kGxLeitJ1R \
  taj9LZ-hhRGi3iBWxaOsXQpO_Lj-tWioGxY_nleuwbpga4 \
  010000000200
Reminder: We use the same --readwrite-accounts tagEPj9hCW8Z9ehMoyJ4UR7faJggQhXb56_8kGxLeitJ1R because we’re modifying the same counter account that was created in the previous step.
Output:
Success: Transaction executed successfully
Signature: ts0dxkQCkCr2pGwhchAD5ub_OfYll1gjCemMkxLl2xHWD0QvkrzQpKFzdBRSI28855HvMoptHBIExh3_37xKE0ABzc
Slot: 568
Compute Units Consumed: 5980
State Units Consumed: 0
Execution Result: 0
VM Error: 0
User Error Code: 0
Events Count: 1
Events Size: 18
Pages Used: 3

Events:
  Event 1: call_idx=0, program_idx=1
    Data (hex): 0100000000000000
The increment instruction is much shorter since it only contains the instruction type (1) and account index (2).Event Explanation: The event shown above is emitted by our program when the counter is successfully incremented. Notice how the counter value went from 0 (initial value after creation) to 1 (after increment). The event data 0100000000000000 represents our new counter value 1 in little endian 64-bit format - this is the value we emitted using tsys_emit_event() in our program code.
🎉 Congratulations! You have successfully:
  • Built a complete Thru program in C
  • Deployed it to the Thru network
  • Created a counter account with state proof
  • Incremented the counter and verified the event emission
Your counter program demonstrates all the essential concepts: account creation, state management, input validation, and event emission.

Next Steps

Now that you’ve mastered the basics, you can:
  • Extend the counter: Add decrement, reset, or set value operations
  • Add access control: Restrict who can modify specific counters
  • Build more complex programs: Try programs that interact with multiple accounts
  • Explore the SDK: Check out advanced syscalls for more complex operations
The patterns you’ve learned here - program structure, input validation, account management, and event emission - form the foundation for any Thru program, regardless of complexity.