# Build Your First Extension

> Start building Flare Confidential Compute extensions.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.

Source: https://dev.flare.network/fcc/guides/getting-started

Build your first **Flare Confidential Compute (FCC)** extension from the Hello World scaffold example. This guide walks you through the setup and testing flow, how extensions fit into the TEE stack, which files you customize, and how to deploy and test on **Coston2** testnet.

Find the scaffold example on [GitHub](https://github.com/flare-foundation/fce-extension-scaffold). For background on FCC and Flare Compute Extensions (FCE), see the [FCC overview](/fcc/overview).

What you will build

A Go HTTP server that runs inside a Trusted Execution Environment (TEE). Onchain callers send instructions through your `InstructionSender` smart contract; the TEE infrastructure relays them to your extension's `POST /action` handler, and you return results that callers poll from the extension proxy.

## Overview[​](#overview "Direct link to Overview")

The scaffold ships a working **GREETING** extension with two commands:

1.  **`SAY_HELLO`** — JSON payload `{"name":"..."}`; returns a greeting string and increments a counter.
2.  **`SAY_GOODBYE`** — ABI-encoded `(name, reason)`; returns a farewell string and increments a counter.

In this guide, you will:

1.  Clone the scaffold and its build dependencies (`tee-node`, `tee-proxy`).
2.  Learn the instruction lifecycle and which files you own.
3.  Deploy the Hello World smart contract and TEE stack against the Coston2 testnet.
4.  Customize the OPType and OPCommand handlers and the onchain entry point for your own extension.

## How an Extension Works[​](#how-an-extension-works "Direct link to How an Extension Works")

An extension does not communicate directly with the chain. A user (or another contract) calls your Solidity `InstructionSender` smart contract, which calls the `TeeExtensionRegistry.sendInstructions()` function. Data providers relay the instruction; your local `ext-proxy` queues it; the TEE node delivers it to your extension.

You control steps that start and end the flow: the onchain `InstructionSender` smart contract and the Go action handlers. Attestation, signing, and message routing are handled by the TEE node and proxy.

## Architecture[​](#architecture "Direct link to Architecture")

The extension stack runs as three Docker services:

-   **`extension-tee` TEE node:** TEE node plus your Go extension (listens for `POST /action` and `GET /state`).
-   **`ext-proxy` proxy server:** Watches Coston2 testnet for instructions targeting your extension, forwards them to the TEE, and exposes results and `/info` on the public proxy URL.
-   **`redis` database:** In-memory store used by the proxy for queue and internal state.

## Clone the Scaffold Example[​](#clone-the-scaffold-example "Direct link to Clone the Scaffold Example")

This example's Docker build scripts expect `tee-node`, `tee-proxy`, and the scaffold as sibling directories under a common parent — the extension module uses a `replace` directive for [`tee-node`](https://github.com/flare-foundation/tee-node), and `start-services.sh` builds [`tee-proxy`](https://github.com/flare-foundation/tee-proxy) unless you set `REGISTRY` to a remote image registry. Clone the repositories in the layout below before you build.

Use the `develop` branch

FCC extension work currently requires the **`develop`** branches of `tee-node` and `tee-proxy` (not `main` or older release tags). Check out `develop` after cloning, as shown below.

```
mkdir -p tee/extension-examplescd teegit clone https://github.com/flare-foundation/tee-node.gitgit -C tee-node checkout developgit clone https://github.com/flare-foundation/tee-proxy.gitgit -C tee-proxy checkout developgit clone https://github.com/flare-foundation/fce-extension-scaffold.git \  extension-examples/extension-scaffoldcd extension-examples/extension-scaffold
```

Your tree should look like this:

```
tee/├── tee-node/              # develop branch├── tee-proxy/             # develop branch└── extension-examples/    └── extension-scaffold/   # working directory for the rest of this guide
```

## Hello World Onchain Smart Contract[​](#hello-world-onchain-smart-contract "Direct link to Hello World Onchain Smart Contract")

The `HelloWorldInstructionSender` smart contract is the only onchain entry point for your operations, and it connects the rest of the flow.

It talks to:

-   **`TeeExtensionRegistry`** — registers extensions and accepts `sendInstructions` calls that require a fee.
-   **`TeeMachineRegistry`** — returns random TEE machine IDs for your extension via `getRandomTeeIds` function.

<details>
<summary>View the code</summary>

View the code

contracts/InstructionSender.sol

```
// SPDX-License-Identifier: MITpragma solidity ^0.8.27;// TODO: Replace local interfaces with imports from flare-smart-contracts-v2 once published as a package.import { ITeeExtensionRegistry } from "./interfaces/ITeeExtensionRegistry.sol";import { ITeeMachineRegistry } from "./interfaces/ITeeMachineRegistry.sol";/// @title HelloWorldInstructionSender/// @author Flare Foundation/// @notice Hello World example — on-chain entry point for sending instructions to the TEE.////// DO NOT MODIFY: constructor, setExtensionId(), _getExtensionId()contract HelloWorldInstructionSender {    /// @notice Operation type for greeting actions (SAY_HELLO, SAY_GOODBYE).    // forge-lint: disable-next-line(unsafe-typecast)    bytes32 public constant OP_TYPE_GREETING = bytes32("GREETING");    /// @notice Command for the SAY_HELLO action.    // forge-lint: disable-next-line(unsafe-typecast)    bytes32 public constant OP_COMMAND_SAY_HELLO = bytes32("SAY_HELLO");    /// @notice Command for the SAY_GOODBYE action.    // forge-lint: disable-next-line(unsafe-typecast)    bytes32 public constant OP_COMMAND_SAY_GOODBYE = bytes32("SAY_GOODBYE");    /// @notice Reference to the TEE extension registry contract.    ITeeExtensionRegistry public immutable TEE_EXTENSION_REGISTRY;    /// @notice Reference to the TEE machine registry contract.    ITeeMachineRegistry public immutable TEE_MACHINE_REGISTRY;    /// @notice First public extension ID. The registry reserves IDs below this    /// for system/reserved extensions; public extensions are assigned from here up.    uint256 private constant FIRST_PUBLIC_EXTENSION_ID = 0x10000; // 65536    uint256 private _extensionId;    /// @notice Payload for the SAY_GOODBYE instruction.    struct SayGoodbyeMessage {        string name;        string reason;    }    /// @notice Initializes the contract with registry addresses.    /// @param _teeExtensionRegistry Address of the TEE extension registry.    /// @param _teeMachineRegistry Address of the TEE machine registry.    constructor(        ITeeExtensionRegistry _teeExtensionRegistry,        ITeeMachineRegistry _teeMachineRegistry    ) {        require(address(_teeExtensionRegistry) != address(0), "TeeExtensionRegistry cannot be zero address");        require(address(_teeMachineRegistry) != address(0), "TeeMachineRegistry cannot be zero address");        require(address(_teeExtensionRegistry).code.length > 0, "TeeExtensionRegistry has no code");        require(address(_teeMachineRegistry).code.length > 0, "TeeMachineRegistry has no code");        TEE_EXTENSION_REGISTRY = _teeExtensionRegistry;        TEE_MACHINE_REGISTRY = _teeMachineRegistry;    }    /// @notice Finds and sets this contract's extension id. Can only be set once.    /// DO NOT MODIFY this function.    function setExtensionId() external {        require(_extensionId == 0, "Extension ID already set.");        uint256 c = TEE_EXTENSION_REGISTRY.nextPublicExtensionId();        for (uint256 i = FIRST_PUBLIC_EXTENSION_ID; i < c; ++i) {            if (TEE_EXTENSION_REGISTRY.getTeeExtensionInstructionsSender(i) == address(this)) {                _extensionId = i;                return;            }        }        revert("Extension ID not found.");    }    /// @notice Sends a SAY_HELLO instruction to the TEE.    /// @param _message JSON-encoded payload (e.g. {"name": "Alice"}).    function sendSayHello(bytes calldata _message) external payable {        address[] memory teeIds = TEE_MACHINE_REGISTRY.getRandomTeeIds(_getExtensionId(), 1);        address[] memory cosigners = new address[](0);        ITeeExtensionRegistry.TeeInstructionParams memory params = ITeeExtensionRegistry.TeeInstructionParams({            opType: OP_TYPE_GREETING,            opCommand: OP_COMMAND_SAY_HELLO,            message: _message,            cosigners: cosigners,            cosignersThreshold: 0,            claimBackAddress: msg.sender        });        TEE_EXTENSION_REGISTRY.sendInstructions{value: msg.value}(            teeIds,            params        );    }    /// @notice Sends a SAY_GOODBYE instruction to the TEE.    /// @param _name The name of the person to say goodbye to.    /// @param _reason The reason for saying goodbye.    function sendSayGoodbye(string calldata _name, string calldata _reason) external payable {        address[] memory teeIds = TEE_MACHINE_REGISTRY.getRandomTeeIds(_getExtensionId(), 1);        address[] memory cosigners = new address[](0);        ITeeExtensionRegistry.TeeInstructionParams memory params = ITeeExtensionRegistry.TeeInstructionParams({            opType: OP_TYPE_GREETING,            opCommand: OP_COMMAND_SAY_GOODBYE,            message: abi.encode(SayGoodbyeMessage({name: _name, reason: _reason})),            cosigners: cosigners,            cosignersThreshold: 0,            claimBackAddress: msg.sender        });        TEE_EXTENSION_REGISTRY.sendInstructions{value: msg.value}(            teeIds,            params        );    }    /// @notice Returns the cached extension ID, reverting if not yet set.    /// @return The extension ID assigned to this contract.    function _getExtensionId() internal view returns (uint256) {        require(_extensionId != 0, "Extension ID is not set.");        return _extensionId;    }}
```

</details>

note

Constructor arguments are the two Flare system registry addresses. On Coston2, they are already deployed; the deploy tooling reads them from `config/coston2/deployed-addresses.json`. This is temporary while FCC is in development. On release, addresses will be available through the [`FlareContractRegistry`](/network/guides/flare-contracts-registry) contract.

## Customizing the Extension[​](#customizing-the-extension "Direct link to Customizing the Extension")

The scaffold is a complete Hello World example. The HTTP server and deploy scripts are ready to use — customize these files so OPType and OPCommand strings stay aligned across Solidity and Go:

#

File

What you change

1

`internal/config/config.go`

OPType and OPCommand string constants and SemVer `Version`

2

`pkg/types/types.go`

Request, response, and state structs

3

`internal/extension/extension.go`

Routing and handlers

4

`pkg/types/register.go`

Decoders for the types server

5

`contracts/InstructionSender.sol`

Matching `bytes32` constants and send functions

6

`tools/cmd/run-test/main.go`

E2E payloads and assertions

### OPType and OPCommand Must Match[​](#optype-and-opcommand-must-match "Direct link to OPType and OPCommand Must Match")

Solidity stores commands as `bytes32("...")`. Go compares hashed string constants with `teeutils.ToHash(...)`. The strings must be identical in all three places:

```
bytes32 constant OP_TYPE_GREETING       = bytes32("GREETING");bytes32 constant OP_COMMAND_SAY_HELLO   = bytes32("SAY_HELLO");bytes32 constant OP_COMMAND_SAY_GOODBYE = bytes32("SAY_GOODBYE");
```

```
const (    OPTypeGreeting      = "GREETING"    OPCommandSayHello   = "SAY_HELLO"    OPCommandSayGoodbye = "SAY_GOODBYE")
```

```
case dataFixed.OPType == teeutils.ToHash(config.OPTypeGreeting):    // then sub-route on OPCommand
```

warning

Mismatch of the OPType or OPCommand strings is the most common cause of `unsupported op type` / `unsupported op command` responses.

### Config Constants[​](#config-constants "Direct link to Config Constants")

internal/config/config.go

```
// Package config contains configuration values and defaults used by the extension.package configimport (	"os"	"strconv"	"time")const (	Version = "0.1.0"	OPTypeGreeting     = "GREETING"	OPCommandSayHello  = "SAY_HELLO"	OPCommandSayGoodbye = "SAY_GOODBYE"	TimeoutShutdown = 5 * time.Second)// Defaults.var (	ExtensionPort  = 8080	SignPort       = 9090	TypesServerPort = 8100)// Environment variables override defaults.func init() {	ep := os.Getenv("EXTENSION_PORT")	sp := os.Getenv("SIGN_PORT")	tp := os.Getenv("TYPES_SERVER_PORT")	if ep != "" {		if v, err := strconv.Atoi(ep); err == nil {			ExtensionPort = v		}	}	if sp != "" {		if v, err := strconv.Atoi(sp); err == nil {			SignPort = v		}	}	if tp != "" {		if v, err := strconv.Atoi(tp); err == nil {			TypesServerPort = v		}	}}
```

Increment `Version` when behavior or the onchain interface changes, because the TEE registration path treats code version as part of the extension lifecycle.

### Request and Response Types[​](#request-and-response-types "Direct link to Request and Response Types")

pkg/types/types.go

```
// Package types contains types that could be useful to other apps when interacting with this extension.package typesimport (	"github.com/ethereum/go-ethereum/accounts/abi"	"github.com/ethereum/go-ethereum/common")// SayHelloRequest is the JSON payload sent via the Solidity contract.type SayHelloRequest struct {	Name string `json:"name"`}// SayHelloResponse is the JSON payload returned in ActionResult.Data.type SayHelloResponse struct {	Greeting       string `json:"greeting"`	GreetingNumber int    `json:"greetingNumber"`}// SayGoodbyeRequest is the ABI-decoded payload sent via the Solidity contract.type SayGoodbyeRequest struct {	Name   string `json:"name"`	Reason string `json:"reason"`}// SayGoodbyeResponse is the JSON payload returned in ActionResult.Data.type SayGoodbyeResponse struct {	Farewell       string `json:"farewell"`	FarewellNumber int    `json:"farewellNumber"`}// SayGoodbyeMessageArg describes the ABI layout of SayGoodbyeMessage from the Solidity contract.var SayGoodbyeMessageArg abi.Argumentfunc init() {	tupleTy, _ := abi.NewType("tuple", "", []abi.ArgumentMarshaling{		{Name: "name", Type: "string"},		{Name: "reason", Type: "string"},	})	SayGoodbyeMessageArg = abi.Argument{Type: tupleTy}}// State holds the extension's observable state, returned by GET /state.type State struct {	GreetingCount int    `json:"greetingCount"`	LastGreeting  string `json:"lastGreeting"`	FarewellCount int    `json:"farewellCount"`	LastFarewell  string `json:"lastFarewell"`}// --- DO NOT MODIFY below this line. ---// StateResponse is the envelope returned by GET /state.type StateResponse struct {	StateVersion common.Hash `json:"stateVersion"`	State        State       `json:"state"`}
```

The `SAY_HELLO` command uses JSON in `OriginalMessage`. The `SAY_GOODBYE` command uses ABI encoding so the contract can accept typed `string` arguments and `abi.encode` them for the TEE.

### Handlers[​](#handlers "Direct link to Handlers")

Each handler follows the same pattern: decode, validate, execute under a mutex if you touch shared state, then call `buildResult`.

Status values in `ActionResult`:

-   **`0`** — error (message in `Log`)
-   **`1`** — success
-   **`≥ 2`** — pending (async work; later POST final result to the node)

internal/extension/extension.go

```
package extensionimport (	"bytes"	"encoding/json"	"fmt"	"net/http"	"sync"	"extension-scaffold/internal/config"	"extension-scaffold/pkg/types"	"github.com/flare-foundation/go-flare-common/pkg/tee/instruction"	"github.com/flare-foundation/go-flare-common/pkg/tee/structs"	teetypes "github.com/flare-foundation/tee-node/pkg/types"	teeutils "github.com/flare-foundation/tee-node/pkg/utils"	"github.com/flare-foundation/tee-node/pkg/processorutils")type Extension struct {	mu     sync.RWMutex	Server *http.Server	greetingCount int	lastGreeting  string	farewellCount int	lastFarewell  string}// --- DO NOT MODIFY: New(), actionHandler() are boilerplate.func New(extensionPort, signPort int) *Extension {	e := &Extension{}	mux := http.NewServeMux()	mux.HandleFunc("GET /state", e.stateHandler)	mux.HandleFunc("POST /action", e.actionHandler)	e.Server = &http.Server{Addr: fmt.Sprintf(":%d", extensionPort), Handler: mux}	return e}// stateHandler() structure is boilerplate but update the State field mapping to match your Extension fields.func (e *Extension) stateHandler(w http.ResponseWriter, r *http.Request) {	e.mu.RLock()	stateResponse := types.StateResponse{		StateVersion: teeutils.ToHash(config.Version),		State: types.State{			GreetingCount: e.greetingCount,			LastGreeting:  e.lastGreeting,			FarewellCount: e.farewellCount,			LastFarewell:  e.lastFarewell,		},	}	e.mu.RUnlock()	err := json.NewEncoder(w).Encode(stateResponse)	if err != nil {		http.Error(w, fmt.Sprintf("sending response: %v", err), http.StatusInternalServerError)		return	}}func (e *Extension) processAction(action teetypes.Action) (int, []byte) {	dataFixed, err := processorutils.Parse[instruction.DataFixed](action.Data.Message)	if err != nil {		return http.StatusBadRequest, []byte(fmt.Sprintf("decoding fixed data: %v", err))	}	switch {	case dataFixed.OPType == teeutils.ToHash(config.OPTypeGreeting):		return e.processGreeting(action, dataFixed)	default:		return http.StatusNotImplemented, []byte(fmt.Sprintf(			"unsupported op type: received %s, expected %s (%s)",			dataFixed.OPType.Hex(), teeutils.ToHash(config.OPTypeGreeting).Hex(), config.OPTypeGreeting,		))	}}// processGreeting routes GREETING instructions by OPCommand.func (e *Extension) processGreeting(action teetypes.Action, df *instruction.DataFixed) (int, []byte) {	switch {	case df.OPCommand == teeutils.ToHash(config.OPCommandSayHello):		ar := e.processSayHello(action, df)		b, _ := json.Marshal(ar)		return http.StatusOK, b	case df.OPCommand == teeutils.ToHash(config.OPCommandSayGoodbye):		ar := e.processSayGoodbye(action, df)		b, _ := json.Marshal(ar)		return http.StatusOK, b	default:		return http.StatusNotImplemented, []byte(fmt.Sprintf(			"unsupported op command: received %s, expected one of [%s (%s), %s (%s)]",			df.OPCommand.Hex(),			teeutils.ToHash(config.OPCommandSayHello).Hex(), config.OPCommandSayHello,			teeutils.ToHash(config.OPCommandSayGoodbye).Hex(), config.OPCommandSayGoodbye,		))	}}// processSayHello handles SAY_HELLO instructions: returns a greeting and tracks count.func (e *Extension) processSayHello(action teetypes.Action, df *instruction.DataFixed) teetypes.ActionResult {	var req types.SayHelloRequest	dec := json.NewDecoder(bytes.NewReader(df.OriginalMessage))	dec.DisallowUnknownFields()	err := dec.Decode(&req)	if err != nil {		return buildResult(action, df, nil, 0, fmt.Errorf("decoding request: %w", err))	}	if req.Name == "" {		return buildResult(action, df, nil, 0, fmt.Errorf("name must not be empty"))	}	e.mu.Lock()	e.greetingCount++	greetingNumber := e.greetingCount	greeting := fmt.Sprintf("Hello, %s! Welcome to Flare Confidential Compute.", req.Name)	e.lastGreeting = greeting	e.mu.Unlock()	resp := types.SayHelloResponse{		Greeting:       greeting,		GreetingNumber: greetingNumber,	}	data, _ := json.Marshal(resp)	return buildResult(action, df, data, 1, nil)}// processSayGoodbye handles SAY_GOODBYE instructions: returns a farewell and tracks count.func (e *Extension) processSayGoodbye(action teetypes.Action, df *instruction.DataFixed) teetypes.ActionResult {	var req types.SayGoodbyeRequest	err := structs.DecodeTo(types.SayGoodbyeMessageArg, df.OriginalMessage, &req)	if err != nil {		return buildResult(action, df, nil, 0, fmt.Errorf("decoding request: %w", err))	}	if req.Name == "" {		return buildResult(action, df, nil, 0, fmt.Errorf("name must not be empty"))	}	e.mu.Lock()	e.farewellCount++	farewellNumber := e.farewellCount	farewell := fmt.Sprintf("Goodbye, %s! Reason: %s", req.Name, req.Reason)	e.lastFarewell = farewell	e.mu.Unlock()	resp := types.SayGoodbyeResponse{		Farewell:       farewell,		FarewellNumber: farewellNumber,	}	data, _ := json.Marshal(resp)	return buildResult(action, df, data, 1, nil)}
```

Leave `New()` and the HTTP wiring in place. Add your own OPType cases in `processAction`, then implement command handlers next to `processSayHello` / `processSayGoodbye`.

### Decoder Registry[​](#decoder-registry "Direct link to Decoder Registry")

Register message and result decoders so tooling and the types server can decode payloads for your commands:

pkg/types/register.go

```
package typesimport "extension-scaffold/pkg/decoder"// RegisterDecoders registers all type decoders for this extension.// Extension developers: add new registrations here for each OPType/OPCommand.func RegisterDecoders(r *decoder.Registry) {	// SAY_HELLO message (JSON)	r.Register(		decoder.RegistryKey{OPType: "GREETING", OPCommand: "SAY_HELLO", Kind: decoder.KindMessage},		decoder.NewJSONDecoder[SayHelloRequest](),	)	// SAY_HELLO result (JSON)	r.Register(		decoder.RegistryKey{OPType: "GREETING", OPCommand: "SAY_HELLO", Kind: decoder.KindResult},		decoder.NewJSONDecoder[SayHelloResponse](),	)	// SAY_GOODBYE message (ABI-encoded)	r.Register(		decoder.RegistryKey{OPType: "GREETING", OPCommand: "SAY_GOODBYE", Kind: decoder.KindMessage},		decoder.NewABIDecoder[SayGoodbyeRequest](SayGoodbyeMessageArg),	)	// SAY_GOODBYE result (JSON)	r.Register(		decoder.RegistryKey{OPType: "GREETING", OPCommand: "SAY_GOODBYE", Kind: decoder.KindResult},		decoder.NewJSONDecoder[SayGoodbyeResponse](),	)}
```

## Deploying and Testing on Coston2[​](#deploying-and-testing-on-coston2 "Direct link to Deploying and Testing on Coston2")

This walkthrough runs a local simulated TEE against the live Coston2 chain (chain ID `114`) using Docker and an HTTPS tunnel. To follow the setup path, configure the `.env` file, the indexer TOML, and `ngrok` before running the core script sequence.

Quick start

After configuring the `.env` file, the indexer TOML, and ngrok, the core script sequence is:

```
./scripts/pre-build.sh./scripts/start-services.sh --chain coston2./scripts/post-build.sh./scripts/test.sh
```

### Step 1: Configure `.env`[​](#step-1-configure-env "Direct link to step-1-configure-env")

```
cp .env.example .env
```

Set at least:

.env

```
DEPLOYMENT_PRIVATE_KEY="<your-funded-coston2-private-key-hex-no-0x>"INITIAL_OWNER="0x<your-address>"PROXY_PRIVATE_KEY="<same-or-another-funded-key>"CHAIN_URL=https://coston2-api.flare.network/ext/C/rpcADDRESSES_FILE=./config/coston2/deployed-addresses.jsonLOCAL_MODE=falseSIMULATED_TEE=trueNORMAL_PROXY_URL=https://tee-proxy-coston2-1.flare.rocksEXT_PROXY_URL=https://<your-tunnel-domain>
```

-   **`LOCAL_MODE=false`** — required on live networks (attestation path enabled).
-   **`SIMULATED_TEE=true`** — use a simulated code hash/platform so you can develop without Confidential VM hardware.
-   **`EXT_PROXY_URL`** — public HTTPS URL of your tunnel to host port **6674** (set in Step 2).

Optionally set TEE governance (used by the node container and by `post-build`):

.env

```
# GOVERNANCE_SIGNERS="0xAbc...,0xDef..."   # comma-separated 0x addresses# GOVERNANCE_THRESHOLD=2
```

If unset, both default to the deployer (`INITIAL_OWNER`) as the sole signer with threshold `1` — fine for Hello World. The node and onchain registry must agree on this set, or TEE registration reverts with `InvalidGovernanceHash`.

### Step 2: Reserve a Public Proxy URL[​](#step-2-reserve-a-public-proxy-url "Direct link to Step 2: Reserve a Public Proxy URL")

The `pre-build`, `post-build`, `start-services`, and `test` scripts all use `EXT_PROXY_URL`. Set it before starting Docker services.

Security — read before exposing port 6674

Exposing port **6674** makes your local **ext-proxy** reachable over HTTPS. Anyone with the tunnel URL can call the proxy HTTP API.

Use a tunnel **only for Coston2 testnet**, and stop it when finished.

In a separate terminal:

```
ngrok http 6674
```

Or with cloudflared:

```
cloudflared tunnel --url http://localhost:6674
```

Copy the HTTPS URL into `EXT_PROXY_URL` in `.env`.

info

Cloudflared quick tunnels generate a new URL on each run — update `EXT_PROXY_URL` whenever the domain changes.

### Step 3: Configure the indexer database[​](#step-3-configure-the-indexer-database "Direct link to Step 3: Configure the indexer database")

The local `ext-proxy` queries Flare's C-chain indexer for TEE events. Copy the Coston2 examples and fill in credentials:

```
cp config/proxy/extension_proxy.coston2.docker.toml.example \  config/proxy/extension_proxy.coston2.docker.tomlcp config/proxy/extension_proxy.coston2.toml.example \  config/proxy/extension_proxy.coston2.toml
```

Edit the `[db]` block:

config/proxy/extension\_proxy.coston2.docker.toml

```
[db]host = "<indexer-db-host>"port = 3306database = "<indexer-db-name>"username = "<indexer-db-user>"password = "<indexer-db-password>"log_queries = false
```

Chain ID `114` and the Coston2 Flare system contract addresses are already set in the example files.

Flare Indexer Access

To get indexer credentials, contact [support](https://flare.network/resources/technical-support) or [X](https://x.com/FlareDevs) and share what you are building.

### Step 4: Deploy the contract and register the extension[​](#step-4-deploy-the-contract-and-register-the-extension "Direct link to Step 4: Deploy the contract and register the extension")

```
./scripts/pre-build.sh
```

This compiles Solidity, deploys the `HelloWorldInstructionSender` smart contract, and registers the extension on the `TeeExtensionRegistry` registry contract. On success, it writes the `EXTENSION_ID` and `INSTRUCTION_SENDER` to the `config/extension.env` file.

warning

Once the `config/extension.env` file exists, pre-build refuses to run again. Use `./scripts/pre-build.sh --force` only when you intentionally want a new extension. Forcing a new pre-build deploys a new sender and registers a new extension ID, while your existing TEE machine may still be tied to the old ID - end-to-end tests then fail with mismatches such as `MachineManager.TooMany()`.

### Step 5: Start the extension stack[​](#step-5-start-the-extension-stack "Direct link to Step 5: Start the extension stack")

```
./scripts/start-services.sh --chain coston2
```

This builds `local/tee-proxy` if needed (from the sibling `tee-proxy` clone), builds `extension-tee` from your scaffold plus the sibling `tee-node` module, and starts the Redis database, the ext-proxy proxy server, and the extension-tee TEE node with the Coston2 compose overlay.

Wait until the proxy is healthy locally:

```
until curl -sf http://localhost:6674/info >/dev/null 2>&1; do sleep 2; doneecho "Extension proxy is ready"
```

Confirm the public tunnel sees the same proxy:

```
source .envcurl -sf "$EXT_PROXY_URL/info" | jq .
```

### Step 6: Verify the proxy[​](#step-6-verify-the-proxy "Direct link to Step 6: Verify the proxy")

```
source .envcurl -s "$EXT_PROXY_URL/info" | jq '.machineData'
```

For a simulated TEE, expect:

Field

Expected

`codeHash`

Simulated hash (`0x194844cf…`)

`extensionId`

Matches `EXTENSION_ID` in `config/extension.env`

`initialOwner`

Matches your `INITIAL_OWNER`

### Step 7: Register the TEE machine[​](#step-7-register-the-tee-machine "Direct link to Step 7: Register the TEE machine")

```
./scripts/post-build.sh
```

This allows your TEE code version for the extension, registers the extension's TEE **governance** onchain (`set-governance`, using `GOVERNANCE_SIGNERS` / `GOVERNANCE_THRESHOLD` from `.env` or the deployer defaults), then registers the TEE machine (using `EXT_PROXY_URL` and `NORMAL_PROXY_URL` from `.env`).

### Step 8: Run the end-to-end test[​](#step-8-run-the-end-to-end-test "Direct link to Step 8: Run the end-to-end test")

```
./scripts/test.sh
```

The test calls `setExtensionId()` if needed, sends `SAY_HELLO` and `SAY_GOODBYE` instructions through the deployed contract, polls the proxy for `ActionResult`s, and asserts the greeting/farewell payloads.

If it passes, your Hello World extension is live against Coston2.

## Making It Your Own[​](#making-it-your-own "Direct link to Making It Your Own")

1.  Choose OPType/OPCommand names that describe your operations.
2.  Update Solidity constants and send functions; regenerate Go bindings if you change the ABI (`./scripts/generate-bindings.sh`).
3.  Implement handlers and types; register decoders.
4.  Update `tools/cmd/run-test/main.go` and re-run `./scripts/test.sh`.
5.  Rebuild the TEE image after code changes: `./scripts/start-services.sh --chain coston2` (compose uses `--build`).

The scaffold README file covers renaming placeholders (`docs/manual-setup.md`). Hooks `scripts/extension-setup.sh` and `scripts/extension-post-setup.sh` are no-ops in Hello World — add deploy-time or post-registration setup there when your extension needs it.

## Troubleshooting[​](#troubleshooting "Direct link to Troubleshooting")

Symptom

What to check

`unsupported op type` / `unsupported op command`

Solidity `bytes32("...")` strings must match Go config constants exactly.

Proxy never becomes ready

Tunnel to **6674**, indexer `[db]` credentials, and `docker compose` logs for `ext-proxy`.

Pre-build refuses to run

`config/extension.env` already exists — use `--force` only if you intend a new extension ID.

`MachineManager.TooMany()` or wrong extension

Extension ID in `config/extension.env` does not match the TEE registered by post-build — avoid casual `--force`.

`InvalidGovernanceHash`

`GOVERNANCE_SIGNERS` / `GOVERNANCE_THRESHOLD` in `.env` must match what the `extension-tee` container received; rebuild/restart after changing them.

Attestation / registration failures

Confirm `LOCAL_MODE=false`, `SIMULATED_TEE=true`, and `NORMAL_PROXY_URL=https://tee-proxy-coston2-1.flare.rocks`.

Docker build cannot find `tee-node`

Confirm the sibling clone layout under `tee/` and that `tee-node` / `tee-proxy` are on the `develop` branch — see [Clone the Scaffold](#clone-the-scaffold-example).

Stop the Coston2 stack when you are done:

```
docker compose -f docker-compose.yaml -f docker-compose.coston2.yaml down
```

What's next?

When you are ready for richer examples, see the [Private Key Extension](/fcc/guides/sign-extension) and [Weather Insurance Extension](/fcc/guides/weather-insurance-extension) guides. Or read the [FCC overview](/fcc/overview) and the [FCC whitepaper](/assets/files/20260706-FlareConfidentialCompute-e488bdb4c8fc5e7dc02ea2b1c890f9c6.pdf).
