Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

A hands-on bootcamp for building confidential computing workflows with Chainlink Runtime Environment (CRE).

🖥️ Environment Setup

Please complete the following steps before the bootcamp to ensure a smooth learning experience.

This Tutorial

This tutorial is available at:

https://smartcontractkit.github.io/CRE-Confidential-bootcamp/

Important Prerequisites

To get the most out of this bootcamp, we recommend preparing the following environment before you start. Some items will be briefly covered in class so we can spend more time on hands-on work.

Required

Optional (only needed for the onchain write exercise)

Note: Both case studies serve deterministic API data from a local mock server, so you can complete every demo without a real LLM API key.

  • 📚 Install mdBook - to build and read this book locally
    cargo install mdbook
    

Reference Code Repository

The two case studies used in this bootcamp come from the official CRE templates repo. We will clone and walk through them together in class:

git clone https://github.com/smartcontractkit/cre-templates.git

The two case studies live at:

Case StudyPath
AI Audit Firewallcre-templates/starter-templates/confidential-workflows/ai-audit-firewall
Automated Liquidation Protectioncre-templates/starter-templates/confidential-workflows/automated-liquidation-protection

Note: You don’t need to read the code ahead of time! We will walk through the key parts line by line during the bootcamp. Just clone the repo in advance.

Welcome to the CRE Confidential Bootcamp

Welcome to the CRE Confidential Bootcamp: Build Confidential Workflows!

This is a two-day, hands-on bootcamp designed to give you a deep, developer-focused guide to building confidential computing workflows with the Chainlink Runtime Environment (CRE).

Videos

This Bootcamp was held live. Here are the recordings

🎤 Instructors

Frank KongFrank Kong
Developer Relations Engineer, Chainlink Labs

X (Twitter): @frank_chainlink
LinkedIn: Frank Kong
Darby MartinezDarby Martinez
Developer Relations Engineer, Chainlink Labs

X (Twitter): @darbease
LinkedIn: Darby Martinez
Solange GueirosSolange Gueiros
Developer Relations Manager (Education & Content),
Chainlink Labs

X (Twitter): @solangegueiros
LinkedIn: Solange Gueiros
solange.dev

Schedule

📅 Day 1: CRE + Confidential Workflow Fundamentals (1.5 hours)

Build a core understanding of CRE and confidential computing, and run your first Confidential Workflow:

  • CRE core concepts and mental model
  • Confidential Workflows: TEEs, enclaves, and the Vault DON
  • Case Study 1: AI Audit Firewall (smart contract audit firewall)
    • Workflow flow and key code walkthrough
    • How Chainlink implements Confidential Workflows
    • Why this use case must run confidentially
  • Case Study 1 demo
  • ❓ Q&A - open questions

📅 Day 2: Hands-On — Automated Liquidation Protection (1.5 hours)

No repeated fundamentals — we jump straight into deploy instructions and the second complete case study:

  • Workflows in production: how to deploy a Hello World workflow
  • Finance primer: what liquidation is and how to prevent it
  • Case Study 2: Automated Liquidation Protection
    • Workflow flow and key code walkthrough
    • Confidential policy parameters and front-running resistance
  • Case Study 2 demo
  • Wrap-up and next steps
  • ❓ Q&A - open questions

What You’ll Build

Two complete applications built on CRE Confidential Workflows (each available in both TypeScript and Go):

Case StudyOne-linerWhat confidentiality protects
AI Audit FirewallBefore a transaction executes, two LLMs audit the target contracts inside an enclave and produce an ALLOW / DENY / MANUAL_REVIEW verdict, optionally written onchainScanner and LLM API credentials, plus the audit process data
Automated Liquidation ProtectionContinuously monitors a lending position’s health and automatically adds collateral or repays debt before liquidation happensExchange credentials, risk thresholds, and defense strategy parameters

Both case studies come from the official CRE templates repo: cre-templates.

Stay Connected

Keep Learning

CRE CLI Quick Setup

Before we start building, let’s make sure your CRE environment is set up correctly. We’ll follow the official guide at cre.chain.link.

Step 1: Create a CRE Account

  1. Visit cre.chain.link
  2. Create an account or log in
  3. Access the CRE platform dashboard

Step 2: Install the CRE CLI

The CRE CLI is essential for compiling and simulating workflows. It compiles your TypeScript code into a WebAssembly (WASM) binary and lets you test workflows locally before deployment.

Option 1: Automatic Installation

The easiest way to install is using the installation script (reference docs):

macOS/Linux

curl -sSL https://cre.chain.link/install.sh | sh

Windows

irm https://cre.chain.link/install.ps1 | iex

Option 2: Manual Installation

If you prefer manual installation, or if the automatic installation doesn’t work for your environment, follow the official Chainlink documentation for your platform:

Verify Installation

cre version

Step 3: Authenticate with the CRE CLI

Link your CLI to your CRE account:

cre login

This opens a browser window for authentication. Once authenticated, your CLI is ready to use.

Check your login status and account info:

cre whoami

Troubleshooting

CRE CLI command not found

If the cre command is not found after installation:

macOS/Linux

# Add to your shell config file (~/.bashrc, ~/.zshrc, etc.)
export PATH="$HOME/.cre/bin:$PATH"

# Reload your shell
source ~/.zshrc  # or ~/.bashrc

Windows

Add the CLI to your PATH

What Can You Do Now?

With CRE set up, you can:

  • Create a new CRE project: Run cre init to get started
  • Compile workflows: The CRE CLI compiles your TypeScript code into a WASM binary
  • Simulate workflows: Test workflows locally with cre workflow simulate — both case studies in this bootcamp run this way
  • Deploy workflows: Deploy to production when ready (Early Access)

Note: Deploying Confidential Workflows is currently in Private Beta and requires separate enrollment (see the Day 2 wrap-up). However, local simulation requires no special access — which is exactly how we run everything in this bootcamp.

What Is CRE

Before writing any code, let’s build a clear mental model of what CRE is and how it works.

What Is CRE?

Chainlink Runtime Environment (CRE) is an orchestration layer that lets you write and run your own workflows in TypeScript or Go, powered by Chainlink Decentralized Oracle Networks (DONs). With CRE, you can compose different capabilities (such as HTTP, onchain reads/writes, signing, and consensus) into verifiable workflows that connect smart contracts to APIs, cloud services, AI systems, other blockchains, and more. These workflows execute on DONs with built-in consensus, acting as a secure, tamper-resistant, and highly available runtime.

The Problem CRE Solves

Smart contracts have a fundamental limitation: they can only see data on their own chain.

  • ❌ Cannot fetch data from external APIs (exchange balances, risk signals)
  • ❌ Cannot call AI models (LLM audits, policy reasoning)
  • ❌ Cannot protect privacy (onchain data and execution are visible to everyone)

CRE bridges this gap by providing a verifiable runtime where you can:

  • ✅ Fetch data from any API (including private data behind API keys)
  • ✅ Call AI services for reasoning and decision-making
  • ✅ Write verified results back onchain
  • ✅ Process sensitive data in a confidential execution environment (the subject of this bootcamp 🔒)

…with cryptographic consensus guaranteeing that every step is verified.

Core Concepts

1. Workflow

A Workflow is the offchain code you develop, written in TypeScript or Go. CRE compiles it to WebAssembly (WASM) and runs it across a Decentralized Oracle Network (DON).

// A workflow is just TypeScript code!
const initWorkflow = (config: Config) => {
  return [
    cre.handler(trigger, callback),
  ]
}

2. Trigger

A Trigger is the event that starts a workflow. CRE supports three types:

TriggerWhen it firesExample
CRONOn a schedule“Check position health every 5 minutes”
HTTPWhen an HTTP request arrives“Start an audit when the API is called”
LogWhen a smart contract emits an event“Settle when SettlementRequested fires”

Both case studies in this bootcamp use the CRON Trigger — the most common trigger for automated monitoring scenarios.

3. Capability

A Capability is what a workflow can do — a microservice that performs a specific task:

CapabilityWhat it does
HTTPMake HTTP requests to external APIs
EVM ReadRead data from smart contracts
EVM WriteWrite data to smart contracts
Confidential HTTPMake HTTP requests from inside a confidential execution environment (URL, headers, and response body are confidential from node operators)

Each capability runs on its own dedicated DON with built-in consensus.

4. Decentralized Oracle Network (DON)

A DON is a network of independent nodes that:

  1. Independently execute your workflow
  2. Compare their results
  3. Reach consensus using a Byzantine Fault Tolerant (BFT) protocol
  4. Return a single, verified result

The Trigger-and-Callback Pattern

This is the core architectural pattern you will use in every CRE workflow:

cre.handler(
  trigger,    // WHEN to execute (cron, http, log)
  callback    // WHAT to execute (your business logic)
)

Each trigger fire starts a fresh, independent, stateless execution: the callback runs, does its work, returns a result, and completes. Inside the callback, you invoke capabilities through SDK clients; each call is asynchronous and returns a consensus-verified result.

Execution Flow

When a trigger fires, here’s what happens:

1. Trigger fires (cron schedule, HTTP request, or on-chain event)
            │
            ▼
2. Workflow DON receives the trigger
            │
            ▼
3. Each node executes your callback independently
            │
            ▼
4. When callback invokes a capability (HTTP, EVM Read, etc.):
            │
            ▼
5. Capability DON performs the operation
            │
            ▼
6. Nodes compare results via BFT consensus
            │
            ▼
7. Single verified result returned to your callback
            │
            ▼
8. Callback continues with trusted data

From “Verifiable” to “Confidential”

The model above is already powerful, but it carries an implicit assumption: your workflow’s code and data run on DON nodes, and node operators can, in principle, inspect what is being computed.

For most applications that’s fine. But if your workflow handles:

  • 🔑 High-value credentials (exchange API keys with trading/withdrawal permissions)
  • 📊 Risk thresholds and strategy parameters that must not be made public
  • 🤖 Proprietary scoring models and the data they reason over

…then you need a Confidential Workflow. That’s the topic of the next section — and the core of this bootcamp.

Key Takeaways

ConceptOne-liner
WorkflowYour automation logic, compiled to WASM
TriggerThe event that starts execution (CRON, HTTP, Log)
CallbackThe function containing your business logic
CapabilityA microservice performing a specific task (HTTP, EVM Read/Write, Confidential HTTP)
DONA set of network nodes executing under consensus
ConsensusThe BFT protocol guaranteeing verified results

What’s Next

Now that you understand the fundamentals of CRE, let’s discover why Confidential Computing and then we’ll dive into how CRE achieves confidential computing through TEEs.

Confidential Workflows: Why Confidential Computing

This section answers three questions:

  • What is a Confidential Workflow?
  • Why do you need one?
  • How Chainlink implements it?

The Problem It Solves

By default, your workflow’s code — along with any secrets or sensitive inputs it processes — runs on Workflow DON nodes, where node operators can, in principle, inspect what it’s computing. That’s fine for most workflows.

But some computation is sensitive on its own:

  • 🎯 Risk thresholds and strategies: if the parameters for when to add collateral or rebalance leak, they can be predicted, front-run, and deliberately exploited
  • 🔑 High-value credentials: exchange API keys with trading or withdrawal permissions, LLM API keys with spending limits, payment network credentials — leaking these is far worse than leaking an ordinary read-only key
  • 🧠 Proprietary models and reasoning: the data processed by scoring models, audit criteria, or decision logic you don’t want third parties to see

Confidential Workflows close this gap: sensitive computation executes inside a secure enclave, designed so that what is actually being computed remains confidential from node operators during execution.

What Is a Confidential Workflow?

A Confidential Workflow is a CRE workflow that designates part of its logic to run inside a running instance of a TEE (Trusted Execution Environment) — an enclave — instead of on Workflow DON nodes.

A TEE is a hardware-isolated execution environment designed so that even the machine’s own operator cannot inspect the computation and data it processes during execution. CRE currently supports AWS Nitro Enclaves.

The key point: a Confidential Workflow is still fundamentally a standard CRE workflow, with an explicit confidential execution path added where you need it. It’s fully compatible with the trigger / callback / capability model you already know, and it’s built, deployed, and operated the same way — you decide what stays inside the enclave and what crosses back out to the Workflow DON for consensus-verified execution (such as generating a signed report to submit onchain).

In CRE, running a workflow confidentially means moving execution of the sensitive part into an enclave, and giving your code a runtime built for that environment (the TeeRuntime):

1. Declare a confidential handler
   Register the handler that should run inside a TEE with
   handlerInTee (TS) / cre.HandlerInTee (Go), specifying which
   TEE types/regions your workflow accepts
            │
            ▼
2. Trigger fires
   The Workflow DON hands the triggered request to an enclave
   instead of executing the callback locally
            │
            ▼
3. Enclave execution
   Your callback runs inside the enclave, receiving a
   TeeRuntime instead of the regular DON runtime
            │
            ▼
4. Dynamic secret fetch
   Secrets are requested and decrypted inside the enclave at
   the moment your code needs them — released by the Vault DON
   directly into the attested enclave
            │
            ▼
5. In-enclave capability calls
   HTTP and other supported calls execute directly from inside
   the enclave — URLs, headers, and response bodies stay
   confidential from node operators; trust comes from enclave
   attestation rather than DON-level consensus
            │
            ▼
6. Crossing back to the DON (optional)
   For anything that needs Workflow DON consensus — like
   generating a signed report via runtime.report() — you
   explicitly cross back out with runtime.usingTheDons()
            │
            ▼
7. Execution completes
   DON consensus verifies the enclave's attestation, proving
   the integrity of the workflow logic that executed within it

In code, there are only two core changes:

import { handlerInTee, type TeeRuntime } from "@chainlink/cre-sdk";

// The callback receives a TeeRuntime
const onCronTrigger = async (runtime: TeeRuntime<Config>): Promise<string> => {
  // Fetch secrets inside the enclave — released by the Vault DON directly into the enclave
  const apiKey = runtime.getSecret({ id: "exchange_api_key" }).result().value;
  // Make confidential HTTP calls from inside the enclave ...
  return "done";
};

const initWorkflow = (config: Config): Workflow<Config> => {
  const cron = new CronCapability();
  return [
    handlerInTee(                          // ← instead of cre.handler
      cron.trigger({ schedule: config.schedule }),
      onCronTrigger,
      [{ tee: "nitro", regions: ["us-west-2"] }],  // ← TEE type + regions
    ),
  ];
};

The Confidentiality Boundary: What’s Protected and What Isn’t

Understanding the confidentiality boundary is critical to designing your workflow correctly:

✅ Protected by default❌ Not automatically protected
Secrets the Vault DON releases into the enclaveWorkflow triggers, chain reads, and chain writes — these always execute on Workflow DON nodes, never inside the enclave
Sensitive inputs and intermediate values you don’t explicitly share outside the enclaveYour workflow’s source code, deployed binary, and orchestration metadata
Capability calls made from inside the enclave (URL / headers / request & response bodies)Capability requests and responses that aren’t routed through the enclave
Enclave execution memory (as long as your computation runs inside it)Reports, transaction calldata, and any output you deliver outside the enclave boundary

⚠️ Workflow logic is not confidential. Your handler’s source code and compiled binary are not confidential just because part of its logic runs inside an enclave. Confidential Workflows protect only the data processed during execution inside the enclave including Vault DON secrets (such as API keys), sensitive HTTP response payloads, and intermediate values not explicitly shared outside the enclave.

When Do Secrets Need Enclave-Level Protection?

Not every secret needs enclave-level protection. A simple rule of thumb: if disclosure would expose more than the workflow needs, or data that will never be made public onchain, it’s a candidate for enclave execution.

High-value secrets — put them in the enclaveRegular DON execution is usually fine
Exchange credentials with trading/withdrawal access, institutional custody credentialsAPI keys for publicly available data (weather, block explorers, public market data)
OAuth client secrets, KMS keys, payment processor, banking, and payment network credentialsPublic wallet addresses
LLM API keys with spending limits, proprietary data provider credentialsOther credentials with similarly limited impact if disclosed

Typical Use Cases

Confidential Workflows apply anywhere the computation behind a decision — not just a workflow’s API calls — needs to remain confidential from node operators:

Use caseWhat confidentiality buys you
AI smart contract audit firewall (Day 1 case study)Audit criteria and third-party API credentials stay inside the enclave
Automated liquidation protection (Day 2 case study)Risk thresholds and the defensive strategy run inside the enclave, designed to prevent them from being predicted and front-run
Automated portfolio rebalancingAllocation policy and trade sizing stay confidential, so the rebalance can’t be anticipated and traded against
Automated tradingA private strategy can’t be copied or traded against, even though the resulting transactions are onchain
Automated payment orchestrationRouting logic and account details stay confidential from node operators
Proprietary data computationBoth the data you can’t expose and the computation over it stay confidential

Key Takeaways

ConceptOne-liner
TEE / EnclaveA hardware-isolated execution environment — even the machine’s operator can’t see the computation inside
Confidential WorkflowA standard CRE workflow that designates part of its logic to run inside an enclave
TeeRuntimeThe runtime given to callbacks executing inside the enclave
Vault DONThe DON that releases secrets directly into the attested enclave
AttestationProof that the enclave is running the expected workflow logic, verified by DON consensus
handlerInTeeThe API for registering a confidential handler (TS; cre.HandlerInTee in Go)
runtime.usingTheDons()Explicitly cross from the enclave back to the DON runtime for consensus operations

📌 Availability: Confidential Workflows are in Private Beta — deployment requires enrollment. But local simulation with the CRE CLI is fully open, and every demo in this bootcamp runs in local simulation.

What’s Next

Enough theory — let’s look at our first real case study: the AI Audit Firewall, a pre-execution security firewall that uses confidential computing to protect its audit credentials and process.

Case Study 1: AI Audit Firewall Overview

Template source: cre-templates/starter-templates/confidential-workflows/ai-audit-firewall

The template is available in TypeScript and Go; the two implementations are behaviorally equivalent. In this bootcamp we are using TypeScript.

The Use Case: A Confidential Pre-Execution Security Firewall

Imagine you’re building a trading product: before a user executes an onchain interaction (say, with an unfamiliar token contract or protocol router), they want an automated line of defense that:

  1. Takes the context of the proposed transaction (target token contract, protocol contract, calldata, etc.)
  2. Fetches both contracts’ source code, ABI, and verification status through a chain scanner
  3. Runs an AI-powered security audit on the contracts, producing structured risk signals
  4. Issues a firewall verdict based on aggregate risk: allow, block, or route for manual review
  5. Persists an audit log, and optionally writes the verdict onchain for contracts to consume

That’s the AI Audit Firewall: a pre-execution security audit workflow driven by two LLMs running inside a confidential execution environment.

Why This Use Case Needs a Confidential Workflow

This workflow handles things that must not leak at every step:

Confidential assetImpact of leakage
🔑 Scanner API credentialsThird parties could abuse your paid scanner quota or spoof scan results
🔑 Both LLM API credentialsKeys with spending limits get stolen — direct financial loss
📄 Fetched contract source code and audit intermediatesThe audit process gets observed; attackers can craft contracts that specifically evade detection
🧠 Audit prompts and evaluation processOnce the audit criteria leak, malicious contracts can be engineered to “pass the test”

With handlerInTee, all of the above stays inside the enclave: credentials are released by the Vault DON directly into the enclave, and the URLs, headers (including API keys), and response bodies of HTTP calls remain confidential from node operators.

Workflow Flow

                    ┌────────────────────────────────────────────────┐
                    │                CRON Trigger fires              │
                    │    (screens a pending transaction every 5 min) │
                    └────────────────────────┬───────────────────────┘
                                             │
                                             ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Stage 1: Ingest the proposed transaction (collectTransactionProposal)    │
│ GET /transaction-proposal → token + protocol contract addresses,         │
│ calldata, signer, ...                                                    │
└────────────────────────────────────────┬─────────────────────────────────┘
                                         │
                                         ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Stage 2: Fetch & validate contract data, confidentially                  │
│ ① GET /credentials/verify  → validate scanner credential scopes          │
│ ② GET /contracts/{address} → source, ABI, compiler version, and          │
│                              verification status for both contracts      │
│ ③ Any contract not verified by the scanner → immediate DENY, log & exit  │
└────────────────────────────────────────┬─────────────────────────────────┘
                                         │
                                         ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Stage 3: Dual-LLM confidential audit                                     │
│ Primary model:   audits the token contract + transaction proposal        │
│ Secondary model: audits the protocol contract, given Primary's findings  │
│ Each returns structured risk signals:                                    │
│   · obfuscatedTax (hidden tax)      · privilegeEscalation                │
│   · externalCallRisk                · logicBomb                          │
│   · recommendation (allow/deny/review) + confidence + reasoning          │
└────────────────────────────────────────┬─────────────────────────────────┘
                                         │
                                         ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Stage 4: Enforce the verdict & keep records                              │
│ ① determineVerdict: merge both models' signals                           │
│    → ALLOW / DENY / MANUAL_REVIEW                                        │
│ ② POST /audit-log       → persist the full audit record                  │
│ ③ POST /firewall-action → enforce (allow / block / manual review)        │
│ ④ (optional) signed report written onchain via EVM Write                 │
└──────────────────────────────────────────────────────────────────────────┘

Visualize the Workflow

Want to explore this workflow yourself? The complete workflow definition is available as a JSON file. You can import it into https://cre.solange.dev/ — a visual GUI tool for CRE workflows — to inspect and interact with the flow step by step.

The Verdict Rules (Conservative Merge)

determineVerdict follows a “better safe than sorry” strategy:

  1. Either model flags any risk signalDENY
  2. Otherwise, if either model recommends review, has confidence below 0.7, or the two models disagree → MANUAL_REVIEW
  3. Otherwise → ALLOW

Project Structure

ai-audit-firewall/                  ← CRE project root
├── project.yaml                    ← Project-level config (RPC endpoints)
├── secrets.yaml                    ← Secret ID → env var mappings
├── .env.example                    ← Environment variable template
├── contracts/                      ← Optional onchain consumer contract
│   ├── AuditFirewallConsumer.sol
│   └── ReceiverTemplate.sol
└── ai-audit-firewall-ts/           ← TypeScript workflow
    ├── main.ts                     ← The workflow code ⭐
    ├── workflow.yaml               ← Workflow settings
    ├── config.staging.json         ← Simulation config (URLs, secret IDs)
    └── mock-server.js              ← Local deterministic mock API server

The Three Secrets

secrets.yaml declares every secret the workflow uses — all fetched inside the enclave:

Secret IDPurpose
scanner_api_keyAccess the contract scanner (fetch source/ABI, verify credentials)
primary_llm_api_keyCall the first audit model
secondary_llm_api_keyCall the second audit model

The Mock Server: No Real APIs Needed

To make the demo fully self-contained, the template ships an Express-based mock server (mock-server.js) that simulates the three kinds of external dependencies locally, exposing only /audit-firewall/* routes:

Mock endpointSimulated role
GET /audit-firewall/transaction-proposalSource of transaction proposals
GET /audit-firewall/scanner/contracts/:addressContract scanner (like an Etherscan-style service)
GET /audit-firewall/scanner/credentials/verifyScanner credential validation
POST /audit-firewall/v1/analysis/primaryThe first LLM audit model
POST /audit-firewall/v1/analysis/secondaryThe second LLM audit model
POST /audit-firewall/audit-logAudit logging service
POST /audit-firewall/firewall-actionFirewall action enforcement

The mock data is deterministic: it ships with a well-behaved MockERC20Token and a MockProtocolRouter carrying an “external calls” note, so we can anticipate the demo outcome. In a real deployment, swap these URLs for the real services — the workflow code doesn’t change.

What’s Next

Now that we’ve seen the overall flow, let’s dive into main.ts and see how a Chainlink Confidential Workflow is actually written.

Case Study 1: Key Code Walkthrough

Open ai-audit-firewall/ai-audit-firewall-ts/main.ts and let’s walk through how a Chainlink Confidential Workflow is implemented. The file is about 770 lines, but there are only 5 core confidential-computing patterns — master them and you’ve mastered Confidential Workflow development.

1. Registering a Confidential Handler with handlerInTee

This is the only entry-point change needed to turn a regular workflow into a Confidential Workflow:

import {
  CronCapability,
  Runner,
  handlerInTee,          // ← API for registering a confidential handler
  type TeeRuntime,       // ← the enclave-specific runtime type
  type Workflow,
} from "@chainlink/cre-sdk";

export const initWorkflow = (config: Config): Workflow<Config> => {
  // ... config validation ...

  const cron = new CronCapability();

  return [
    handlerInTee(
      cron.trigger({ schedule: config.schedule }),   // ① Trigger: unchanged
      onCronTrigger,                                  // ② Callback: receives a TeeRuntime
      [{ tee: "nitro", regions: ["us-west-2"] }],     // ③ TEE requirements: AWS Nitro + region
      {
        preHook: (cfg: Config) => buildRestrictions(cfg), // ④ Capability restrictions (see §5)
      },
    ),
  ];
};

Key points:

  • handlerInTee(trigger, callback, teeRequirements, options): declares that this handler’s callback must execute inside a TEE, and specifies the acceptable TEE types (nitro) and regions.
  • The callback signature changes from Runtime<Config> to TeeRuntime<Config> — which exposes in-enclave capabilities (such as the enclave release path of getSecrets, and confidential HTTP calls) plus the interface for crossing back to the DON.
  • As with a regular workflow, the Workflow DON still listens for the CRON trigger; when it fires, the DON hands execution to the enclave.

2. Fetching Secrets Inside the Enclave (the Vault DON)

export const runAuditFirewall = async (
  runtime: TeeRuntime<Config>,
  client = new HTTPClient(),
): Promise<string> => {
  const { mock_base_url, scanner_url, primary_llm_url, secondary_llm_url, secrets_ids } = runtime.config;

  // ① One batched call fetches all 3 secrets at once — released by the Vault
  //    DON directly into the attested enclave at the moment your code needs them
  const secrets = runtime
    .getSecrets([
      { id: secrets_ids.scanner_api_key_id },
      { id: secrets_ids.primary_llm_api_key_id },
      { id: secrets_ids.secondary_llm_api_key_id },
    ])
    .result();

  // ② Look each value up by its secret ID from the batched result
  const scannerApiKey = secrets[secrets_ids.scanner_api_key_id].value;
  const primaryLlmApiKey = secrets[secrets_ids.primary_llm_api_key_id].value;
  const secondaryLlmApiKey = secrets[secrets_ids.secondary_llm_api_key_id].value;

  runtime.log("audit-firewall-getsecrets-ok");
  // ...

Key points:

  • The runtime.getSecrets([...]) call happens inside the enclave, and the plaintext secrets never pass through Workflow DON nodes — the Vault DON releases them directly into the enclave after verifying the enclave’s attestation. Batching all 3 secrets into one call is more efficient than issuing 3 separate getSecret calls.
  • Secret IDs are not hardcoded; they’re injected via the secrets_ids field in config.staging.json. In local simulation, the actual secret values come from the .env file at the project root (mapped through secrets.yaml).

3. Confidential HTTP Calls

Every outbound request in the workflow goes through the same HTTPClient. Because the calls originate inside the enclave, the URL, headers (including API keys), and response body all remain confidential from node operators:

const getJson = (
  runtime: TeeRuntime<Config>,
  client: HTTPClient,
  url: string,
  headers: Record<string, string>,
): Record<string, unknown> => {
  const response = client
    .sendRequest(runtime, {
      url,
      method: "GET",
      headers,                      // ← carries x-scanner-api-key, confidential
    })
    .result();

  const raw = decodeBody(response.body);
  if (response.statusCode >= 400) {
    throw new Error(`request failed status=${response.statusCode} body=${raw}`);
  }
  return parseJson(raw);            // ← the response body stays inside the enclave
};

POST requests work the same way, except the body must be base64-encoded first:

const bodyBytes = new TextEncoder().encode(JSON.stringify(body));
const encodedBody = Buffer.from(bodyBytes).toString("base64");

const response = client
  .sendRequest(runtime, {
    url,
    method: "POST",
    body: encodedBody,
    headers,
  })
  .result();

What Is the Transaction Data Being Fetched?

The first call the workflow makes (GET /transaction-proposal, via collectTransactionProposal) retrieves the key fields of a transaction that has not been submitted onchain yet — it is a proposed transaction about to be sent/executed. You can see this from the TransactionProposal type (lines 40–51 of main.ts):

type TransactionProposal = {
  chain_selector: number;
  chain_name: string;
  tx_hash: string;
  from_address: string;
  token_contract_address: string;
  protocol_contract_address: string;
  calldata: string;
  value_wei: string;
  signer: string;
  requested_action: string;
};
FieldMeaning
chain_selector / chain_nameThe target chain
tx_hashThe proposal’s transaction hash
from_address / signerThe initiator and the signer
token_contract_addressThe token contract involved
protocol_contract_addressThe protocol contract to interact with
calldata / value_weiThe call data and the amount of native token being transferred
requested_actionThe action being requested (e.g., transfer)

This is exactly the “pre-execution” nature of the firewall: the workflow screens the transaction before it ever touches the chain, and the two contract addresses above become the audit targets in the next stage.

A Key Defense in Stage 2: Validate Credentials Before Trusting Data

Before trusting the contract data returned by the scanner, the workflow validates the credential’s own permission scopes — an easily overlooked but professional security detail:

const validateScannerCredentials = (runtime, client, scannerUrl, scannerApiKey) => {
  const response = getJson(runtime, client, `${scannerUrl}/credentials/verify`, {
    ...JSON_HEADERS,
    "x-scanner-api-key": scannerApiKey,
  });

  const validation = parseScannerCredentialValidation(response);
  const hasVerificationScope = validation.scopes.includes("verification:read");
  const hasContractScope = validation.scopes.includes("contracts:read");

  // Invalid credential or missing required scopes → throw and abort this execution
  if (!validation.valid || !hasVerificationScope || !hasContractScope) {
    throw new Error(`scanner credentials failed validation ...`);
  }
  return validation;
};

4. The Dual-LLM Audit and the Conservative Verdict

Two Models, Two Perspectives

// Primary: audits the token contract + transaction proposal
const primaryAnalysis = requestAuditModel(
  runtime,
  client,
  primary_llm_url,
  primaryLlmApiKey,
  "audit-primary",
  buildPrimaryPrompt(proposal, tokenContract),
);

// Secondary: audits the protocol contract, given Primary's findings as prior context
const secondaryAnalysis = requestAuditModel(
  runtime,
  client,
  secondary_llm_url,
  secondaryLlmApiKey,
  "audit-secondary",
  buildSecondaryPrompt(proposal, tokenContract, protocolContract, primaryAnalysis),
);

Each model is required to emit strict JSON: four risk signals (obfuscatedTax, privilegeEscalation, externalCallRisk, logicBomb) + a recommendation (allow/deny/review) + confidence + reasoning.

Merging the Verdicts: Better Safe Than Sorry

export const determineVerdict = (primary, secondary): FirewallVerdict => {
  const combinedFlags = mergeFlags(primary.riskFlags, secondary.riskFlags);

  // Either model flags any risk signal → block
  if (hasMaliciousRisk(combinedFlags)) {
    return "DENY";
  }

  // Either model is unsure / low confidence / models disagree → manual review
  const reviewRequested = primary.recommendation === "review" || secondary.recommendation === "review";
  const lowConfidence = primary.confidence < 0.7 || secondary.confidence < 0.7;
  if (reviewRequested || lowConfidence || primary.recommendation !== secondary.recommendation) {
    return "MANUAL_REVIEW";
  }

  return "ALLOW";
};

Here’s how the “better safe than sorry” mechanism plays out:

Trigger conditionResultWhy it’s conservative
Any risk flag from either model is true (mergeFlags is pure OR logic)DENYNo consensus needed — one model flagging a problem is enough
Either model recommends reviewMANUAL_REVIEWIf one auditor isn’t sure, there’s no automatic pass
Either model has confidence < 0.7MANUAL_REVIEWEven an “allow” conclusion doesn’t count if the model isn’t confident
The two models’ recommendations disagreeMANUAL_REVIEWDisagreement between auditors = not trustworthy

Notice that ALLOW is the hardest verdict to reach: it requires both models to agree on “allow,” both with confidence ≥ 0.7, and zero risk flags between them.

The workflow then writes the full context to the audit log (POST /audit-log) and triggers the firewall action (POST /firewall-action) — both requests also carry confidential credentials and originate inside the enclave.

5. Capability Restrictions: Least Privilege

handlerInTee’s preHook lets you declare a strict capability allowlist for the execution — even if the workflow code were tampered with, it couldn’t invoke capabilities or secrets beyond the declaration:

export const buildRestrictions = (config: Config) => {
  const httpRestrictor = new HTTPClientRestrictor();
  const capabilityRestrictions = [
    httpRestrictor.limitSendRequest(8),          // HTTP: max 8 calls
    { method: { id: CONSENSUS_CAPABILITY_ID, method: "Report", maxCalls: 1 } }, // Report: max 1 call
  ];

  // If EVM writes are configured, limit writeReport to 1 call
  const evmConfig = config.evms?.[0];
  if (evmConfig?.chain_selector_name) { /* ... */ }

  return {
    capabilities: {
      type: "CAPABILITY_RESTRICTION_TYPE_CLOSED",  // closed allowlist
      maxTotalCalls: 10,
      restrictions: capabilityRestrictions,
    },
    secrets: {
      maxSecrets: 3,                                // max 3 secrets
      restrictions: [
        { exactSecret: { id: secrets_ids.scanner_api_key_id, namespace: "main" } },
        { exactSecret: { id: secrets_ids.primary_llm_api_key_id, namespace: "main" } },
        { exactSecret: { id: secrets_ids.secondary_llm_api_key_id, namespace: "main" } },
      ],
    },
  };
};

This is defense in depth for confidential computing: the enclave keeps data invisible to nodes, while restrictions constrain what the workflow “is allowed to do.”

6. Crossing Back to the DON: Writing the Verdict Onchain

So far, everything has stayed inside the enclave. But writing the verdict onchain (an operation that requires DON consensus) means explicitly crossing back to the regular DON runtime:

const writeVerdictOnChain = async (runtime, result): Promise<string | undefined> => {
  const evmConfig = runtime.config.evms?.[0];
  if (!evmConfig) return undefined;

  const network = getNetwork({ chainFamily: "evm", chainSelectorName: evmConfig.chain_selector_name });
  const evmClient = new EVMClient(network.chainSelector.selector);

  // Encode the verdict as an ABI payload: (uint8 verdictCode, uint8 riskMask, uint64 chainSelector)
  const reportPayload = encodeVerdictReport(result, BigInt(network.chainSelector.selector));

  // ★ Explicitly cross back to the Workflow DON: generate a DON-signed report
  const donRuntime = runtime.usingTheDons();
  const reportResponse = donRuntime
    .report({
      encodedPayload: hexToBase64(reportPayload),
      encoderName: "evm",
      signingAlgo: "ecdsa",
      hashingAlgo: "keccak256",
    })
    .result();

  // Deliver the signed report to the consumer contract via the Forwarder
  const writeResult = evmClient
    .writeReport(donRuntime, {
      receiver: evmConfig.consumer_address,
      report: reportResponse,
      gasConfig: { gasLimit: evmConfig.gas_limit },
    })
    .result();

  if (writeResult.txStatus !== TxStatus.SUCCESS) {
    throw new Error(`onchain write failed with status ${writeResult.txStatus}`);
  }
  return bytesToHex(writeResult.txHash || new Uint8Array(32));
};

This is exactly what the confidentiality boundary design means:

  • What crosses out of the enclave is your explicit choice — here, only three encoded values cross (verdict code, risk mask, chain selector); all audit process data stays inside the enclave.
  • runtime.usingTheDons() returns a runtime for operations that need DON consensus; once data passes through it, it’s handled like any non-confidential capability call.
  • Onchain, AuditFirewallConsumer.sol receives the report: it extends ReceiverTemplate and decodes (uint8, uint8, uint64) in _processReport, recording the Verdict (Allow/Deny/ManualReview).
function _processReport(bytes calldata report) internal override {
    (uint8 verdictCode, uint8 riskMask, uint64 chainSelector) =
        abi.decode(report, (uint8, uint8, uint64));
    // verdictCode: 1=Allow, 2=Deny, 3=ManualReview
    ...
    emit VerdictReceived(verdict, riskMask, chainSelector);
}

7. Putting It Together: The Main Flow

export const runAuditFirewall = async (runtime, client = new HTTPClient()) => {
  // ① Fetch 3 secrets inside the enclave (1 batched getSecrets call)
  // ② GET /transaction-proposal          — get the proposed transaction
  // ③ GET /credentials/verify            — validate scanner credentials
  // ④ GET /contracts/{token} /{protocol} — fetch contract source & ABI
  //    └─ any contract unverified → DENY + log, early return
  // ⑤ POST /v1/analysis/primary          — LLM #1 audits the token contract
  // ⑥ POST /v1/analysis/secondary        — LLM #2 audits the protocol contract
  // ⑦ determineVerdict                   — conservative merge
  // ⑧ POST /audit-log + /firewall-action — record & enforce
  // ⑨ writeVerdictOnChain                — cross back to the DON (optional)
  // ⑩ Return the JSON result
};

Recap: Why Confidentiality Is Necessary

Now consider what this case study would look like without a Confidential Workflow:

RiskRegular workflowConfidential Workflow
LLM / scanner API keysPlaintext passes through DON node memory, visible to node operatorsReleased by the Vault DON directly into the enclave; invisible to nodes
Contract source code and audit intermediatesVisible to nodesProcessed only in enclave memory
Audit requests (URL / headers / body)Visible to nodesConfidential HTTP; invisible to nodes
Audit criteria exploited to evade detectionPossibleThe evaluation process never leaves the enclave

What’s Next

With the code explained, let’s run it — start the mock server and simulate this workflow with the CRE CLI!

Case Study 1: Demo

Let’s run the AI Audit Firewall end to end. It takes only 4 steps: clone the repo → configure the environment → start the mock server → simulate.

Step 1: Clone the Templates Repo

git clone https://github.com/smartcontractkit/cre-templates.git
cd cre-templates/starter-templates/confidential-workflows/ai-audit-firewall

Step 2: Set Up Environment Variables

Create a .env at the project root (ai-audit-firewall/):

cp .env.example .env

.env.example is pre-filled with demo defaults:

# Ethereum private key (optional for local simulate; required for real chain writes)
CRE_ETH_PRIVATE_KEY=

MOCK_PORT=8787

MOCK_SCANNER_API_KEY=mock-scanner-key
MOCK_PRIMARY_LLM_API_KEY=mock-primary-llm-key
MOCK_SECONDARY_LLM_API_KEY=mock-secondary-llm-key

Note: These three MOCK_* values are the “secrets” the workflow uses during simulation. The CRE CLI injects them as secrets according to the secrets.yaml mapping. In a real deployment they’d be replaced with real scanner and LLM credentials, managed by the Vault DON.

Step 3: Install Dependencies and Start the Mock Server

cd ai-audit-firewall-ts
bun install
bun run mock:server

You’ll see:

Mock server running at http://127.0.0.1:8787

Keep this terminal running — every API request will hit it.

Open a second terminal:

cd cre-templates/starter-templates/confidential-workflows/ai-audit-firewall/ai-audit-firewall-ts
bun run typecheck
bun run test

The tests cover the verdict logic (determineVerdict), the capability restrictions (buildRestrictions), and the full main flow executed against a mocked HTTP client — reading the tests first is also a great way to understand this workflow.

Step 5: Simulate the Workflow

Back at the project root, start the simulation with the CRE CLI:

cd ..
cre workflow simulate ./ai-audit-firewall-ts --target=staging-settings

Note: Simulation compiles the workflow to WASM and runs it on your machine, but it makes real calls to the mock server’s HTTP endpoints. A CRON-triggered workflow executes once directly in simulation.

You’ll see output similar to:

[SIMULATION] Simulator Initialized

[USER LOG] audit-firewall-getsecrets-ok
[USER LOG] audit-firewall-scanner-credentials-ok provider=mock-scanner scopes=contracts:read,verification:read
[USER LOG] audit-firewall-onchain-report-start
[USER LOG] audit-firewall-onchain tx_hash=0x...
[USER LOG] audit-firewall-complete verdict=ALLOW audit_log_id=audit_...

Workflow Simulation Result:
 "{\"verdict\":\"ALLOW\",\"reasoning\":\"...\",\"riskFlags\":{...}, ...}"

[SIMULATION] Execution finished signal received

Reading the Output Line by Line

Log lineMeaning
audit-firewall-getsecrets-okAll 3 secrets were successfully fetched inside the (simulated) enclave via a single batched getSecrets call
audit-firewall-scanner-credentials-okScanner credentials validated, with contracts:read and verification:read scopes
audit-firewall-onchain-report-startStarted generating the DON-signed report (crossing back to the DON runtime)
audit-firewall-onchain tx_hash=...Simulated onchain write completed (no --broadcast, so nothing actually goes onchain)
audit-firewall-complete verdict=...The final verdict and audit log ID

Why the Default Data Yields ALLOW

The two contracts built into the mock server — MockERC20Token and MockProtocolRouter — are both verified, “clean” contracts with no malicious traits. Both mock LLMs return allow with confidence 0.93 (≥ 0.7) and agree with each other, so under the determineVerdict rules the final verdict is ALLOW.

The mock LLMs are deterministic, keyword-based rule implementations — not real models — which keeps demo results reproducible. To plug in real LLMs, swap primary_llm_url / secondary_llm_url in config.staging.json for real endpoints.

Hands-On Experiments (Optional)

Experiment 1: Make a Contract “Unverified” → Trigger DENY

Edit mock-server.js and change MockERC20Token’s verified: true to false. Restart the mock server and simulate again:

verdict=DENY  reason="One or more contracts are not verified by the scanner."

The workflow aborts early at Stage 2: no LLM calls at all — it logs the audit record and executes the DENY firewall action immediately. When verification fails, there’s nothing left to audit.

Experiment 2: A Real Onchain Write

  1. Deploy contracts/AuditFirewallConsumer.sol to Sepolia (the constructor argument is the CRE Forwarder address)
  2. Fill the deployed address into evms[0].consumer_address in config.staging.json
  3. Set CRE_ETH_PRIVATE_KEY in .env
  4. Run cre workflow simulate ./ai-audit-firewall-ts --target=staging-settings --broadcast

--broadcast makes the simulator execute a real onchain write transaction, after which you can read lastVerdict onchain.

Required configuration: for local simulation on Sepolia, deploy the consumer contract with the mock Forwarder contract address 0x15fC6ae953E024d975e77382eEeC56A9101f9F88 as the constructor argument. Forwarder addresses for other networks are listed in the Forwarder Directory.

Recap: What Just Happened

Mock Server (local port 8787)
   │  ① proposal ② contract data ③ credential check ④ dual-LLM audit ⑤ log & firewall action
   ▼
CRE Simulator
   │  Compiles main.ts → WASM, executes along the (simulated) enclave path
   │  Secrets injected via the secrets.yaml mapping
   ▼
(optional) Sepolia consumer contract ← DON-signed report

🎉 Day 1 Complete!

You have successfully:

  • ✅ Learned CRE’s core concepts (Workflow / Trigger / Capability / DON)
  • ✅ Learned how Confidential Workflows work: TEEs, enclaves, the Vault DON, attestation
  • ✅ Mastered the 5 core patterns of confidential development: handlerInTee, in-enclave batched getSecrets, confidential HTTP, capability restrictions, and usingTheDons()
  • ✅ Run your first Confidential Workflow

Tomorrow we move on to the second case study — Automated Liquidation Protection — and see how confidential strategy parameters defend against front-running. See you then!

Hello World: Your First Workflow

Template source: cre-templates/starter-templates/hello-confidential-workflows (available in TypeScript and Go; the two implementations are behaviorally equivalent)

This is the smallest possible Confidential Workflow end to end, to be focused in the deploy part. It takes only 4 steps:

  1. clone the repo
  2. configure the environment variables
  3. simulate
  4. deploy

What This Workflow Does

This template is the minimal end-to-end shape of a Confidential Workflow, in four steps:

StepWhat it demonstratesAPI
1Register a handler whose callback runs inside a secure enclavecre.handlerInTee(trigger, fn, tees)
2Fetch a secret released by the Vault DON, inside the enclaveruntime.getSecret({ id })
3Make an HTTP call from inside the enclave (request and response stay confidential)HTTPClient.sendRequest(teeRuntime, req)
4Cross back to the Workflow DON for anything needing consensusruntime.usingTheDons()

Concretely, on every CRON tick the workflow:

  1. Hands the triggered request to an enclave (AWS Nitro, us-west-2) instead of executing the callback on Workflow DON nodes
  2. Fetches the API_TOKEN secret — released by the Vault DON directly into the attested enclave
  3. Calls the configured URL from inside the enclave with the secret in the Authorization header
  4. Scores the confidential response against scoreThreshold → verdict APPROVE / REJECT
  5. Crosses back to the DON with usingTheDons() and generates a signed report containing only the verdict and score — never the secret or the raw response body

The default endpoint is https://postman-echo.com/headers, which echoes request headers back — no signup or real API key needed. The workflow uses it to confirm the secret really was injected inside the enclave, reported as the boolean secret reached API: true rather than by ever logging the token.

Prepare the Workflow

Step 1: Clone the Templates Repo

We will use the TypeScript version.

git clone https://github.com/smartcontractkit/cre-templates.git
cd cre-templates/starter-templates/hello-confidential-workflows/hello-confidential-workflows-ts

Step 2: Install Dependencies

bun install --cwd ./my-workflow

Step 3: Set Up Environment Variables

Copy the example environment variables file to a new file named .env.

cp .env.example .env

In this basic example, using the default echo endpoint, the SECRET_API_TOKEN in .env can be any non-empty value, it doesn’t have to be a real API token.

The secrets.yaml at the project root maps the workflow-facing secret ID to that environment variable:

secretsNames:
    API_TOKEN:
        - SECRET_API_TOKEN

Note: In local simulation the CRE CLI injects secret values from .env according to this mapping. In a real deployment, the same secret ID (API_TOKEN) is resolved from the Vault DON instead — your workflow code doesn’t change. Deployment requires an extra step, which will also be covered later.

Simulate the Workflow

From the project root, start the simulation with the CRE CLI:

cre workflow simulate my-workflow --target staging-settings --non-interactive --trigger-index 0

You’ll see output similar to:

[SIMULATION] Running trigger trigger=cron-trigger@1.0.0
╭────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Trigger requested TEE Execution your trigger will run in one of the following Tees:                │
│     - AWS Nitro in us-west-2                                                                       │
│ The simulator is not a real TEE, and is meant to debug.                                            │
│ Do not use it for sensitive information.                                                           │
│ During real execution, user logs for this trigger will not be visible, and will not leave the TEE. │
│ They are presented in the simulator for debugging only.                                            │
╰────────────────────────────────────────────────────────────────────────────────────────────────────╯

[USER LOG] Enclave computation complete. verdict=REJECT

✓ Workflow Simulation Result:
"REJECT (score: 371, secret reached API: true)"

Reading the Output

  • The simulator confirms the TEE constraint it resolved (AWS Nitro in us-west-2) and warns that it is not a real enclave — logs are shown for debugging only; in real execution they never leave the TEE.
  • secret reached API: true means the Vault DON secret was fetched inside the enclave and arrived in the outbound request’s Authorization header.
  • The verdict can flip between APPROVE and REJECT from run to run — the score derives from the live response body, and the echo endpoint includes a per-request trace ID.
  • Lower scoreThreshold in my-workflow/config.staging.json to see APPROVE consistently.

Deploy the Workflow

Deployment takes 3 steps:

  1. add the secret to the Vault DON
  2. deploy
  3. verify.

Private Registry

We’ll use the private registry (authorized by your CRE login session — no wallet, no gas).

The private registry is a Chainlink-hosted, offchain workflow registry.

All lifecycle operations (deploy, activate, pause, delete, update) are authorized by your CRE login session. You do not need to settup a wallet and there are no Ethereum Mainnet transactions and no gas fees for registry management.

Step 1: Add the Secret to the Vault DON (Before Deploying!)

A deployed workflow cannot read your local .env file — it fetches secrets from the Vault DON at runtime.

Before deploying, you must store API_TOKEN in the Vault DON. Execute the secret creation:

cre secrets create secrets.yaml --target staging-settings --secrets-auth=browser

Alert Make sure SECRET_API_TOKEN is set in your .env before executing the command above!

The CLI reads secrets.yaml, picks up the value from SECRET_API_TOKEN in .env, opens a browser window to authorize against the Vault DON with your CRE login session, and stores the secret. When it’s over, you’ll see:

Secret created: secret_id=API_TOKEN, owner=<your-organization-owner>, namespace=main

Verify it landed (only the ID is shown, never the value):

cre secrets list --target staging-settings --secrets-auth=browser

Note: In a Confidential Workflow, the Vault DON releases this secret only into an attested enclave at the moment getSecret() runs — it is never exposed in plaintext to Workflow DON nodes.

Step 2: Deploy

Verify if the configuration file workflow.yaml is already prepared for deployment.

Go to staging-settings, user-workflow.

Add or update deployment-registry: "private":

staging-settings:
  user-workflow:
    workflow-name: "hello-confidential-staging"
    deployment-registry: "private"

Then deploy from the project root:

cre workflow deploy my-workflow --target staging-settings

The CLI compiles the workflow to WASM, uploads the artifacts, and registers the workflow — active immediately:

Deploying Workflow: hello-confidential-staging
Compiling workflow...
✓ Workflow compiled successfully
Uploading files...
✓ Workflow registered in private registry

Details:
   Registry:         private
   Workflow Name:    hello-confidential-staging
   Workflow ID:      <workflow-id>
   Status:           Active
   Owner:            <your-organization-owner>

Step 3: Verify and Manage

Confirm it’s registered and active:

cre workflow list --registry private

You can also check it out on CRE workflows

Manage your workflow:

cre workflow pause my-workflow --target staging-settings     # pause
cre workflow activate my-workflow --target staging-settings  # resume
cre workflow delete my-workflow --target staging-settings    # permanently remove

The workflow now runs on its CRON schedule: every execution happens inside a real enclave, fetches API_TOKEN from the Vault DON, and produces a DON-signed report.

⚠️ Production reminder: the template logs Enclave computation complete. verdict=... inside the enclave for debugging. Remove every runtime.log() inside the TEE handler before any real deployment — anything logged from within a Confidential Workflow could leak the data the enclave is meant to protect.

Clean up

This workflow is intended solely to cover the deployment process, so it is best practice to remove it after learning the process:

cre workflow delete my-workflow --target staging-settings

Key Takeaways

ConceptOne-liner
cre workflow simulateRuns the workflow locally along a simulated enclave path — secrets come from .env
cre secrets createStores secrets in the Vault DON — required before deploying
cre workflow deployCompiles, uploads, and registers the workflow on a DON
Private registryChainlink-hosted registry authorized by your CRE login — no wallet or gas
Vault DONReleases secrets directly into the attested enclave at execution time

What’s Next

You’ve just learned how to deploy a Confidential Workflow — now let´s go to the second use case: Automated Liquidation Protection.

Liquidation and How to Prevent It

No more CRE or Confidential Workflow fundamentals today — we jump straight into the new case study. But because it involves quite a few DeFi finance concepts, let’s spend some minutes getting them clear first.

Overcollateralized Lending: The Foundation of DeFi Borrowing

In DeFi lending protocols like Aave and Compound, borrowing works through overcollateralization:

You deposit $10,000 worth of ETH as collateral
        │
        ▼
The protocol lets you borrow up to a certain ratio (say, $7,000 USDC)
        │
        ▼
Your collateral must always "sufficiently cover" your debt

Why overcollateralization? Because the protocol has no identity information about you and no way to chase you for repayment — the collateral is the only guarantee.

Three Key Metrics

1. LTV (Loan-to-Value)

LTV = debt value / collateral value

Example:

  • deposit $10,000 of ETH
  • borrow $7,000 USDC
  • LTV = $7,000 / $10,000 = 70%.

Each collateral asset has a maximum LTV (say, 75%) that determines how much you can borrow at most.

2. Liquidation Threshold

The liquidation threshold is a line slightly above the max LTV (say, 78%).

When your LTV crosses the liquidation threshold, the position is deemed undercollateralized, and anyone can liquidate it.

3. Health Factor (HF) ⭐

This is the most commonly used risk metric:

                     collateral value × liquidation threshold
Health Factor (HF) = ────────────────────────────────────────
                               debt value
HF valuePosition status
HF > 1Safe, sufficiently collateralized
HF = 1The liquidation line! Can be liquidated on arrival
HF < 1Undercollateralized, can be liquidated

Example:

  • $10,000 of ETH collateral
  • Liquidation threshold: 78%
  • Loan $7,000 debt
  • HF = 10000 × 0.78 / 7000 ≈ 1.11.

⚠️ HF moves with prices. ETH price drops → collateral value shrinks → HF falls → danger when it approaches 1.0. This is what drives “liquidation cascades” during periods of high crypto market volatility.

What Liquidation Costs You

When HF < 1, a liquidator can:

  1. Repay part of your debt on your behalf (say, 50%)
  2. Seize collateral worth the repaid amount plus a bonus (the liquidation bonus, typically 5%–10%) at a discount

For the borrower, liquidation means:

  • 💸 Liquidation penalty: the collateral seized is worth more than the debt repaid
  • 📉 Forced selling at the bottom: your collateral is sold during a market crash — precisely the worst price
  • 🔒 Loss of the position: if the market rebounds afterward, you no longer have collateral to benefit

How to Prevent Liquidation

The core idea is one sentence:

raise your HF before it gets close to 1.

There are two broad approaches:

Approach 1: Increase collateral (grow the numerator)

ActionDescription
add_collateralAdd collateral directly using stablecoin reserves
bridge_and_add_collateralBridge assets from another chain, then add
swap_reserve_to_collateralSwap reserves into the collateral asset, then deposit

Approach 2: Reduce debt (shrink the denominator)

ActionDescription
repay_with_reservesRepay part of the debt directly with reserves
swap_reserve_to_borrowed_and_repaySwap reserves into the borrowed asset, then repay
partial_debt_repaymentRepay a percentage (say, 18%) of the debt
full_debt_repaymentRepay in full, eliminating the risk entirely

Manual vs. Automated

The problem with manual defense: if liquidations happen at 3 AM, within minutes. When price crashes, going from HF 1.15 to liquidated can take just minutes — far too fast for a human to react.

So you need automation — a system that monitors risk signals 24/7 and executes defensive actions as danger approaches. That’s exactly where CRE shines.

Why the Defense Strategy Needs to Be “Confidential”

Automated liquidation protection has a subtle game-theoretic problem:

If your defense strategy is public, it can be exploited.

  • If the market knows “this address adds collateral whenever HF drops below 1.25,” attackers can manipulate prices against you, anticipate your moves, and front-run them.
  • If your reserve size and deployable capital caps are public, an adversary can calculate exactly “how much capital it takes to push you past the liquidation line”.
  • Your exchange credentials and strategy parameters (target HF, deployment caps, sequencing preferences) are all high-value intelligence.

So a production-grade automated liquidation protection system needs:

  1. Automation: 24/7 monitoring + automatic execution → CRE Workflow
  2. Confidentiality: thresholds, strategy, and credentials hidden from node operators → Confidential Workflow

Key Takeaways

ConceptOne-liner
OvercollateralizationDeposit collateral worth more than what you borrow
LTVThe debt / collateral ratio
Liquidation thresholdLTV crossing it → can be liquidated
Health Factor (HF)(collateral × liquidation threshold) / debt; < 1 means danger
LiquidationA liquidator repays your debt and seizes discounted collateral + a bonus
DefenseAdd collateral (numerator ↑) or repay debt (denominator ↓)
Confidential defenseKeep the strategy and thresholds secret so they can’t be predicted or front-run

What’s Next

With the concepts clear, let’s look at Case Study 2: an Automated Liquidation Protection system built with a CRE Confidential Workflow.

Case Study 2: Automated Liquidation Protection Overview

Template source: cre-templates/starter-templates/confidential-workflows/automated-liquidation-protection

The template is available in TypeScript and Go; the two implementations are behaviorally equivalent. In this bootcamp we are using TypeScript.

The Use Case: Confidential Automated Defense Before Liquidation

This workflow provides continuous risk monitoring and automated defense for a DeFi lending position:

  1. Periodically fetches the position’s risk snapshot: collateral/debt prices, health factor, liquidation proximity, LTV, market volatility, plus your available capital (stablecoin reserves, cash balance)
  2. Inside a confidential environment, computes a risk score using your private policy parameters, and asks an LLM policy engine for a defense decision
  3. Validates the decision against hard policy constraints (deployment caps, reserve floors, …)
  4. Executes defensive actions in your preferred order: add collateral, repay debt, or a combination of both

Everything happens before liquidation arrives — automatically pulling the health factor back into the safe zone during high volatility.

Why This Use Case Must Run Confidentially

This is the signature use case in the Confidential Workflows documentation, and it protects even more than Case Study 1:

Confidential assetImpact of leakage
🔑 Exchange API credentialsCredentials that can query your account balances and reserves get stolen
🔑 LLM API credentialsKeys with spending limits get stolen
🎯 Risk thresholds (warning line, min/target health factor)Predicted → front-run, deliberately pushed toward the liquidation line
💰 Capital parameters (reserve deployment cap, minimum reserve balance, collateral allocation cap)Adversaries calculate the exact boundary of your defensive capacity
🔀 Preferred venuesYour defensive moves get traded against in advance

Notice an interesting design choice: the policy parameters are themselves secrets. This workflow has 10 secrets, 8 of which are not “credentials” in the traditional sense but thresholds and strategy — all released by the Vault DON into the enclave. Node operators have no way to learn “how much you can deploy.” (The one exception is the collateral-first / debt-first / balanced sequencing choice — that’s read from public workflow config, not the Vault DON, so only which venues you act through stays confidential, not the order you act in.)

Workflow Flow

                    ┌──────────────────────────────────────────────┐
                    │              CRON Trigger fires              │
                    │      (evaluates the position every 5 min)    │
                    └──────────────────────┬───────────────────────┘
                                           │
                                           ▼
┌────────────────────────────────────────────────────────────────────────┐
│ Stage 0: Fetch 10 secrets inside the enclave (1 getSecrets call)       │
│ Exchange credential + LLM credential + 8 policy-parameter secrets      │
│ (thresholds / caps / preferred venues; sequencing is public config)    │
└──────────────────────────────────────┬─────────────────────────────────┘
                                       │
                                       ▼
┌────────────────────────────────────────────────────────────────────────┐
│ Stage 1: Observe risk signals (collectRiskSnapshot)                    │
│ GET /risk-state → collateral/debt prices, health factor, liquidation   │
│ proximity, LTV, liquidation threshold, volatility, USDC reserve, cash  │
└──────────────────────────────────────┬─────────────────────────────────┘
                                       │
                                       ▼
┌────────────────────────────────────────────────────────────────────────┐
│ Stage 2: Confidential policy reasoning                                 │
│ ① computeRiskScore: deterministic risk score                           │
│    = proximity risk + LTV buffer risk + health risk + volatility risk  │
│ ② Package risk + riskScore + policy into the prompt                    │
│ ③ POST /v1/responses → the LLM policy engine returns a decision:       │
│    shouldDefend + reasoning + actions[]                                │
└──────────────────────────────────────┬─────────────────────────────────┘
                                       │
                                       ▼
┌────────────────────────────────────────────────────────────────────────┐
│ Stage 3: Hard policy checks (enforcePolicy)                            │
│ · per-execution spend ≤ max_reserve_deployment                         │
│ · post-action reserve ≥ min_reserve_balance (breach → throw)           │
│ · partial repayment % ≤ max_partial_debt_repayment_pct                 │
│ · order actions per execution_sequence_preference                      │
└──────────────────────────────────────┬─────────────────────────────────┘
                                       │
                                       ▼
┌────────────────────────────────────────────────────────────────────────┐
│ Stage 4: Execute the defense plan                                      │
│ · proximity ≤ warning threshold and actions exist                      │
│   → POST /execute-defense → "DEFENDED"                                 │
│ · otherwise → log the reason and return "SAFE"                         │
└────────────────────────────────────────────────────────────────────────┘

Visualize the Workflow

Want to explore this workflow yourself? The complete workflow definition is available as a JSON file. You can import it into https://cre.solange.dev/ — a visual GUI tool for CRE workflows — to inspect and interact with the flow step by step.

The Seven Defensive Actions

Recapping the two broad approaches from the previous section, the workflow’s full action set:

TypeActionCategory
add_collateralAdd collateral directly with reservesCollateral side
bridge_and_add_collateralBridge assets cross-chain, then addCollateral side
swap_reserve_to_collateralSwap reserves into the collateral asset, then depositCollateral side
repay_with_reservesRepay debt directly with reservesDebt side
swap_reserve_to_borrowed_and_repaySwap reserves into the borrowed asset, then repayDebt side
partial_debt_repaymentRepay a percentage of the debtDebt side
full_debt_repaymentRepay the debt in fullDebt side

Project Structure

automated-liquidation-protection/         ← CRE project root
├── project.yaml                          ← Project-level config (RPC endpoints)
├── secrets.yaml                          ← 10 secret ID → env var mappings
├── .env.example                          ← Env var template (with all policy defaults)
└── automated-liquidation-protection-ts/  ← TypeScript workflow
    ├── main.ts                           ← The workflow code ⭐
    ├── workflow.yaml                     ← Workflow settings
    ├── config.staging.json               ← Simulation config (URLs, secret ID mappings)
    └── mock-server.js                    ← Local deterministic mock API server

The Ten Secrets

Secret IDTypePurpose
exchange_api_keyCredentialAccess account data (risk snapshot, execute defense)
openai_api_keyCredentialCall the LLM policy engine
liquidation_liquidation_warning_action_thresholdPolicyLiquidation proximity warning line (default 18%)
liquidation_minimum_health_factorPolicyMinimum health factor (default 1.25)
liquidation_target_health_factorPolicyTarget health factor (default 1.5)
liquidation_maximum_stablecoin_reserve_deploymentPolicyMax reserve deployment per execution, $ (default 5000)
liquidation_minimum_stablecoin_reserve_balancePolicyMinimum reserve balance, $ (default 2000)
liquidation_maximum_collateral_allocationPolicyCollateral allocation cap, % (default 80)
liquidation_maximum_partial_debt_repaymentPolicyPartial repayment cap, % (default 40)
liquidation_preferred_venuesPolicyPreferred venue list (default binance,onchain,coinbase)

The sequencing preference (default collateral-first) is not in this table — it’s supplied via public workflow config, not the Vault DON.

The Mock Server

Same pattern as Case Study 1: an Express mock server simulates the external dependencies locally, exposing only /liquidation/* routes:

Mock endpointSimulated role
GET /liquidation/risk-stateExchange/account data source (risk snapshot)
POST /liquidation/v1/responsesOpenAI-style LLM policy engine
POST /liquidation/execute-defenseDefense action execution endpoint

The built-in default risk snapshot is a position already in the danger zone:

  • Collateral: ETH $45,000
  • Debt: $32,000 USDC
  • Health factor: 1.14
  • Liquidation proximity: 12% (warning line: 18%)
  • LTV: 71% / threshold 78%
  • Volatility: 0.37
  • USDC reserve: $10,000
  • Cash: $12,500

→ Liquidation proximity of 12% has already breached the 18% warning line, so the workflow should spring into action.

What’s Next

Let’s open main.ts and see how most of the policy parameters are injected as secrets (one — the sequencing preference — comes from public config instead), and how deterministic constraints “backstop” the LLM’s decisions.

Case Study 2: Key Code Walkthrough

Open automated-liquidation-protection/automated-liquidation-protection-ts/main.ts (about 640 lines). All the confidential patterns from yesterday are reused here, so we’ll focus on what’s new in this case study: policy-as-secrets, deterministic guardrails backing up the LLM, and action sequencing.

1. Confidential Handler Registration (Recap)

import {
  CronCapability,
  HTTPClient,
  NITRO_REGIONS,
  Runner,
  handlerInTee,
  type TeeRuntime,
  type Workflow,
} from "@chainlink/cre-sdk";

export const initWorkflow = (config: Config): Workflow<Config> => {
  // ... config and secrets_ids validation ...

  const cron = new CronCapability();

  return [
    handlerInTee(
      cron.trigger({ schedule: config.schedule }),
      onCronTrigger,
      [{ tee: "nitro", regions: [NITRO_REGIONS[0]] }],
    ),
  ];
};

Exactly the same pattern as Case Study 1: CRON trigger + Nitro enclave execution. The differences start inside the callback.

2. Policy-as-Secrets: 1 getSecrets Call (10 Secrets)

This is the case study’s core design decision — keep every policy parameter in the Vault DON, not in the config file or the code. All 10 secrets are fetched together in a single batched getSecrets call rather than 10 separate getSecret round trips:

export const onCronTrigger = async (runtime: TeeRuntime<Config>): Promise<string> => {
  const { mock_base_url, openai_url, openai_model, secrets_ids } = runtime.config;

  const secrets = runtime
    .getSecrets([
      { id: secrets_ids.exchange_api_key_id },
      { id: secrets_ids.openai_api_key_id },
      { id: secrets_ids.liquidation_warning_action_threshold_secret_id },
      { id: secrets_ids.minimum_health_factor_secret_id },
      { id: secrets_ids.target_health_factor_secret_id },
      { id: secrets_ids.maximum_stablecoin_reserve_deployment_secret_id },
      { id: secrets_ids.minimum_stablecoin_reserve_balance_secret_id },
      { id: secrets_ids.maximum_collateral_allocation_secret_id },
      { id: secrets_ids.maximum_partial_debt_repayment_secret_id },
      { id: secrets_ids.preferred_venues_secret_id },
    ])
    .result();

  // ① Two traditional credentials
  const exchangeApiKey = secrets[secrets_ids.exchange_api_key_id].value;
  const openAiApiKey = secrets[secrets_ids.openai_api_key_id].value;

  // ② Seven numeric policy-parameter secrets
  const liquidationWarningActionThreshold = parseRequiredSecretNumber(
    secrets[secrets_ids.liquidation_warning_action_threshold_secret_id].value,
    secrets_ids.liquidation_warning_action_threshold_secret_id,
  );
  const minimumHealthFactor = parseRequiredSecretNumber(
    secrets[secrets_ids.minimum_health_factor_secret_id].value,
    secrets_ids.minimum_health_factor_secret_id,
  );
  const targetHealthFactor = parseRequiredSecretNumber(
    secrets[secrets_ids.target_health_factor_secret_id].value,
    secrets_ids.target_health_factor_secret_id,
  );
  const maxStablecoinReserveDeployment = parseRequiredSecretNumber(
    secrets[secrets_ids.maximum_stablecoin_reserve_deployment_secret_id].value,
    secrets_ids.maximum_stablecoin_reserve_deployment_secret_id,
  );
  const minStablecoinReserveBalance = parseRequiredSecretNumber(
    secrets[secrets_ids.minimum_stablecoin_reserve_balance_secret_id].value,
    secrets_ids.minimum_stablecoin_reserve_balance_secret_id,
  );
  const maxCollateralAllocation = parseRequiredSecretNumber(
    secrets[secrets_ids.maximum_collateral_allocation_secret_id].value,
    secrets_ids.maximum_collateral_allocation_secret_id,
  );
  const maxPartialDebtRepayment = parseRequiredSecretNumber(
    secrets[secrets_ids.maximum_partial_debt_repayment_secret_id].value,
    secrets_ids.maximum_partial_debt_repayment_secret_id,
  );

  // ③ NOT a secret — sequencing preference now comes straight from public workflow config
  const defensiveSequencePreference = parseExecutionSequencePreference(
    runtime.config.defensive_action_sequencing_preference,
  );  // "collateral-first" | "debt-first" | "balanced"

  // ④ List-typed preferred_venues secret has a dedicated parser
  const preferredVenues = parseVenueListSecret(
    secrets[secrets_ids.preferred_venues_secret_id].value,
  );  // ["binance", "onchain", "coinbase"]

  runtime.log("liquidation-getsecrets-ok");

Why do it this way?

  • Parameters written in the config are visible to anyone who can read the workflow configuration; placed in the Vault DON, they only ever appear inside the enclave.
  • Batching into one getSecrets call resolves all 10 secrets (2 credentials + 8 policy-parameter secrets) in a single round trip to the Vault DON, instead of 10 individual getSecret calls.
  • Numeric secrets are validated with parseRequiredSecretNumber (must be a finite number or it throws) — fail fast on malformed secret content instead of making decisions with a bad threshold.
  • defensive_action_sequencing_preference is the one exception: it’s read directly from runtime.config, not from getSecrets — it’s plain workflow configuration, not Vault DON-protected.
  • These values are then assembled into the in-memory Policy object used throughout the execution.

3. The Risk Snapshot and the Deterministic Risk Score

const client = new HTTPClient();
const risk = collectRiskSnapshot(runtime, client, mock_base_url, exchangeApiKey);
// GET /risk-state → prices, HF, liquidation proximity, LTV, volatility, reserves... (confidential HTTP)

// ... policy object assembled from the secrets fetched above ...

const riskScore = computeRiskScore(risk, policy);

computeRiskScore is a deterministic scoring function — no LLM involved, reproducible and auditable:

export const computeRiskScore = (risk: RiskState, policy: Policy): number => {
  // The closer to liquidation, the higher the score (+5 points per 1% closer)
  const proximityRisk = Math.max(0, policy.liquidation_warning_action_threshold - risk.liquidation_proximity_pct) * 5;
  // Adds points as LTV approaches the liquidation threshold (with a 5% buffer)
  const ltvBufferRisk = Math.max(0, risk.loan_to_value_pct - (risk.liquidation_threshold_pct - 5)) * 2;
  // Heavy penalty when the health factor is below the minimum (+1 point per 0.01 below)
  const healthRisk = Math.max(0, policy.minimum_health_factor - risk.collateral_health_factor) * 100;
  // Volatility weighting
  const volatilityRisk = risk.volatility_index * 25;

  return proximityRisk + ltvBufferRisk + healthRisk + volatilityRisk;
};

Let’s compute it with the mock data: proximity (18−12)×5=30 + LTV 0 + health (1.25−1.14)×100=11 + volatility 0.37×25=9.25 → 50.25. Meaningful risk — defense is warranted.

4. The LLM as a Policy Engine

const prompt = createOpenAiPrompt(risk, riskScore, policy);
// { objective: "Return strict JSON...", policy, risk, riskScore }

const llmResponse = postJson(
  runtime,
  client,
  openai_url,
  {
    model: openai_model,
    input: [
      {
        role: "system",
        content:
          "You are a liquidation-defense policy engine. Emit strict JSON only with key names exactly as requested.",
      },
      {
        role: "user",
        content: prompt,
      },
    ],
  },
  {
    ...JSON_HEADERS,
    // ← confidential header: only ever readable inside the enclave
    Authorization: `Bearer ${openAiApiKey}`,
  },
);

const decision = parseLlmDecision(extractOpenAiText(llmResponse));
// → { shouldDefend, reasoning, actions[] }

Notice the prompt design: the entire policy object is sent straight to the LLM. In a non-confidential workflow this would be dangerous (strategy leakage), but inside the enclave the request and response bodies stay confidential end to end — which is exactly why this case study must run confidentially.

5. Deterministic Guardrails for the LLM (The Gem of This Case Study) ⭐

The LLM can propose, but it is not trusted unconditionally. enforcePolicy applies deterministic rules to hard-check and correct the LLM’s output:

export const enforcePolicy = (
  decision: LiquidationDecision,
  policy: Policy,
  risk: RiskState,
): ExecutableAction[] => {
  if (!decision.shouldDefend || decision.actions.length === 0) {
    return [];
  }

  let projectedReserve = risk.usdc_reserve;
  const executable: ExecutableAction[] = [];

  for (const action of decision.actions) {
    const amount = Math.max(0, action.amountUsd ?? 0);
    const repayPctRaw = Math.max(0, action.repayPct ?? 0);
    const cappedRepayPct = Math.min(repayPctRaw, policy.max_partial_debt_repayment_pct);

    // ① Collateral actions: cap against max_reserve_deployment_usdc, then check the reserve floor
    if (
      action.type === "add_collateral" ||
      action.type === "bridge_and_add_collateral" ||
      action.type === "swap_reserve_to_collateral"
    ) {
      const capped = Math.min(amount, policy.max_reserve_deployment_usdc);
      projectedReserve -= capped;

      if (projectedReserve < policy.min_reserve_balance_usdc) {
        throw new Error(
          `action ${action.type} breaches reserve floor: projected ${projectedReserve.toFixed(2)} < floor ${policy.min_reserve_balance_usdc}`,
        );
      }

      executable.push({
        ...action,
        amountUsd: capped,
        repayPct: 0,
        venue: chooseVenue(action, policy.preferred_venues),
      });
      continue;
    }

    // ② Reserve-funded repayment actions: same cap + reserve floor check as ①
    if (
      action.type === "repay_with_reserves" ||
      action.type === "swap_reserve_to_borrowed_and_repay"
    ) {
      const capped = Math.min(amount, policy.max_reserve_deployment_usdc);
      projectedReserve -= capped;

      if (projectedReserve < policy.min_reserve_balance_usdc) {
        throw new Error(
          `action ${action.type} breaches reserve floor: projected ${projectedReserve.toFixed(2)} < floor ${policy.min_reserve_balance_usdc}`,
        );
      }

      executable.push({
        ...action,
        amountUsd: capped,
        repayPct: 0,
        venue: chooseVenue(action, policy.preferred_venues),
      });
      continue;
    }

    // ③ Partial repayment: % capped by max_partial_debt_repayment_pct; amount comes from outstanding debt, not the reserve
    if (action.type === "partial_debt_repayment") {
      const boundedRepayPct = Math.min(cappedRepayPct, 100);
      executable.push({
        ...action,
        amountUsd: (risk.outstanding_debt_usd * boundedRepayPct) / 100,
        repayPct: boundedRepayPct,
        venue: chooseVenue(action, policy.preferred_venues),
      });
      continue;
    }

    // ④ Full repayment: NOT capped by max_reserve_deployment_usdc — repays the entire debt — but still checked against the reserve floor
    if (action.type === "full_debt_repayment") {
      const amountUsd = risk.outstanding_debt_usd;
      projectedReserve -= amountUsd;

      if (projectedReserve < policy.min_reserve_balance_usdc) {
        throw new Error(
          `action ${action.type} breaches reserve floor: projected ${projectedReserve.toFixed(2)} < floor ${policy.min_reserve_balance_usdc}`,
        );
      }

      executable.push({
        ...action,
        amountUsd,
        repayPct: 100,
        venue: chooseVenue(action, policy.preferred_venues),
      });
      continue;
    }

    executable.push({
      ...action,
      amountUsd: amount,
      repayPct: 0,
      venue: chooseVenue(action, policy.preferred_venues),
    });
  }

  // ⑤ Order the actions per the sequencing preference
  return orderActions(executable, policy.execution_sequence_preference);
};

This is the classic “AI + rules” architecture: the LLM generates a plan in a complex situation, and deterministic code holds the line on capital safety. Even if the LLM hallucinates an action to “deploy $1,000,000,” it gets truncated by the cap or stopped by the red-line exception. Note the asymmetry on full_debt_repayment: it skips the deployment cap entirely (it must repay 100% of the debt, not a truncated amount), but it’s still stopped cold by the reserve-floor red line if the payoff would drain reserves too far.

Action Sequencing Preferences

const priorities = {
  "collateral-first": { add_collateral: 1, bridge_and_add_collateral: 2, ..., full_debt_repayment: 7 },
  "debt-first":       { repay_with_reserves: 1, ..., add_collateral: 5, ... },
  "balanced":         { partial_debt_repayment: 1, bridge_and_add_collateral: 2, repay_with_reserves: 3, ... },
};

The same defense plan can execute in a completely different order for different users, driven by defensive_action_sequencing_preference. Unlike the numeric thresholds and preferred-venues list, this value is read from public workflow config rather than the Vault DON — it’s the one policy parameter that isn’t a secret.

6. Execute the Defense, or Declare SAFE

// Liquidation proximity still above the warning line,
// or no executable actions after policy checks → SAFE
if (risk.liquidation_proximity_pct > policy.liquidation_warning_action_threshold || actions.length === 0) {
  runtime.log(
    `liquidation-no-action proximity=${risk.liquidation_proximity_pct.toFixed(3)} threshold=${policy.liquidation_warning_action_threshold} reason=${decision.reasoning}`,
  );
  return "SAFE";
}

// Otherwise, execute the defense plan
const defenseResponse = postJson(
  runtime,
  client,
  `${mock_base_url}/execute-defense`,
  {
    riskScore,
    liquidationProximityPct: risk.liquidation_proximity_pct,
    reasoning: decision.reasoning,
    actions,
  },
  {
    ...JSON_HEADERS,
    "x-exchange-api-key": exchangeApiKey,
  },
);

runtime.log(
  `liquidation-defense-executed action_count=${actions.length} execution_id=${asString(defenseResponse.execution_id, "unknown")}`,
);

return JSON.stringify({
  status: "DEFENDED",
  actionCount: actions.length,
  riskScore,
  executionId: asString(defenseResponse.execution_id, "unknown"),
});

Only two possible endings: SAFE (no action needed) or DEFENDED (N actions executed) — clear, monitorable, and alertable.

7. Side-by-Side with Case Study 1

DimensionCase 1: AI Audit FirewallCase 2: Liquidation Protection
Confidential assetsScanner + LLM credentialsCredentials + 8 policy-parameter secrets (sequencing preference is public config)
Decision-makingDual LLMs + conservative merge rulesLLM decision + deterministic guardrails
Capability restrictionspreHook allowlist (HTTP ≤ 8, Report ≤ 1, secrets ≤ 3)None declared (default limits)
Data leaving the enclaveVerdict code + risk mask (optionally onchain)Only the defense action instructions (to the execution endpoint)
Number of LLMs2 (Primary / Secondary)1 policy engine

Recap: Why Confidentiality Is Necessary

RiskRegular workflowConfidential Workflow
Risk thresholds (when you act)Visible to nodes → predictable and exploitableEnter the enclave only, as secrets
Capital caps (how much you can deploy)Visible to nodes → exposes your defensive boundaryEnter the enclave only, as secrets
Execution preferences (how you act)Visible to nodes → front-runnableRead from public workflow config, not a Vault secret — an intentional exception; capital caps and thresholds still stay enclave-only
Exchange / LLM credentialsPlaintext passes through node memoryReleased by the Vault DON directly into the enclave
Risk snapshot (your position and reserves)Visible to nodesConfidential HTTP responses stay inside the enclave

What’s Next

Time to run it! Start the mock server and watch the workflow reach a DEFENDED decision on the default risk data.

Case Study 2: Demo

The flow is the same as yesterday: clone the repo → configure the environment → start the mock server → simulate. If you already cloned cre-templates on Day 1, skip straight to Step 2.

Step 1: Clone the Templates Repo

git clone https://github.com/smartcontractkit/cre-templates.git
cd cre-templates/starter-templates/confidential-workflows/automated-liquidation-protection

Step 2: Set Up Environment Variables

Create a .env at the project root:

cp .env.example .env

.env.example is pre-filled with demo defaults — note that it contains the 8 policy-parameter secrets (the sequencing preference is not here; it’s set directly in config.staging.json as plain JSON config, not a Vault secret):

### REQUIRED ENVIRONMENT VARIABLES - SENSITIVE INFORMATION                  ###
### DO NOT STORE RAW SECRETS HERE IN PLAINTEXT IF AVOIDABLE                 ###
### DO NOT UPLOAD OR SHARE THIS FILE UNDER ANY CIRCUMSTANCES                ###
###############################################################################
# Ethereum private key or 1Password reference (e.g. op://vault/item/field)
CRE_ETH_PRIVATE_KEY=

MOCK_PORT=8787
MOCK_EXCHANGE_API_KEY=mock-exchange-key
MOCK_OPENAI_API_KEY=mock-openai-key

MOCK_LIQUIDATION_WARNING_ACTION_THRESHOLD=18
MOCK_LIQUIDATION_MINIMUM_HEALTH_FACTOR=1.25
MOCK_LIQUIDATION_TARGET_HEALTH_FACTOR=1.5
MOCK_LIQUIDATION_MAX_STABLECOIN_RESERVE_DEPLOYMENT=5000
MOCK_LIQUIDATION_MIN_STABLECOIN_RESERVE_BALANCE=2000
MOCK_LIQUIDATION_MAX_COLLATERAL_ALLOCATION=80
MOCK_LIQUIDATION_MAX_PARTIAL_DEBT_REPAYMENT=40
MOCK_LIQUIDATION_PREFERRED_VENUES=binance,onchain,coinbase

Food for thought: why do these values live in .env / secrets rather than in config.staging.json? — Because they’re your private strategy. In a real deployment they’re managed by the Vault DON and only ever appear inside the enclave. The one exception is the sequencing preference: it’s not in .env at all — it’s set directly in config.staging.json ("defensive_action_sequencing_preference": "collateral-first"), because it’s plain workflow config, not a Vault secret.

Step 3: Install Dependencies and Start the Mock Server

cd automated-liquidation-protection-ts
bun install
bun run mock:server

You’ll see:

Mock server running at http://127.0.0.1:8787

Keep this terminal running.

Open a second terminal:

cd cre-templates/starter-templates/confidential-workflows/automated-liquidation-protection/automated-liquidation-protection-ts
bun run typecheck
bun run test

The tests focus on computeRiskScore and enforcePolicy — the two deterministic functions. This illustrates an important principle: LLM output is hard to test, but the rules that hold the bottom line must be 100% testable.

Step 5: Simulate the Workflow

Back at the project root:

cd ..
cre workflow simulate ./automated-liquidation-protection-ts --target=staging-settings

You’ll see output similar to:

[SIMULATION] Simulator Initialized

[USER LOG] liquidation-getsecrets-ok
[USER LOG] liquidation-defense-executed action_count=2 execution_id=defense_...

Workflow Simulation Result:
 "{\"status\":\"DEFENDED\",\"actionCount\":2,\"riskScore\":50.25,\"executionId\":\"defense_...\"}"

[SIMULATION] Execution finished signal received

Reading the Output Line by Line

Log lineMeaning
liquidation-getsecrets-okAll 10 secrets (2 credentials + 8 policy-parameter secrets) fetched inside the enclave; the sequencing preference is read separately from public config
liquidation-defense-executedThe defense plan was executed: 2 actions, with an execution ID

Why the Default Data Triggers DEFENDED

The mock’s built-in position has already breached the warning line:

Liquidation proximity: 12%  <  warning line 18%   → action required
Health factor:         1.14 <  minimum 1.25        → action required

The mock LLM returns this defense decision:

{
  "shouldDefend": true,
  "reasoning": "Liquidation proximity is elevated; add collateral and reduce debt exposure within policy limits.",
  "actions": [
    { "type": "add_collateral",        "amountUsd": 3200, "venue": "onchain" },
    { "type": "partial_debt_repayment", "repayPct": 18,   "venue": "binance" }
  ]
}

Then enforcePolicy performs its deterministic checks:

  • add_collateral $3,200 ≤ per-execution cap $5,000 ✅; post-action reserve 10,000 − 3,200 = $6,800 ≥ floor $2,000 ✅
  • partial_debt_repayment 18% ≤ cap 40% ✅ → converted amount $32,000 × 18% = $5,760
  • Ordered per collateral-first: add_collateral first, then partial_debt_repayment

Finally POST /execute-defense executes both actions → DEFENDED. The risk score of 50.25 matches what we hand-computed in the code walkthrough — verify it yourself.

Hands-On Experiments (Optional)

Experiment 1: Make the Position Safe → Trigger SAFE

Edit mock-server.js and change liquidation_proximity_pct from 12 to 30 (proximity 30% > warning line 18%). Restart the mock server and simulate again:

[USER LOG] liquidation-no-action proximity=30.000 threshold=18 reason=Position is sufficiently healthy; no defensive action required.

Workflow Simulation Result:
 "SAFE"

Experiment 2: Hit the Reserve Red Line

# Raise the minimum reserve balance to 9000 — any deployment breaches the floor
MOCK_LIQUIDATION_MIN_STABLECOIN_RESERVE_BALANCE=9000

The simulation now fails with a breaches reserve floor error thrown by enforcePolicy — the red-line rule’s hard constraint in action.

Recap: What Just Happened

Mock Server (local port 8787)
   │  ① risk snapshot ② LLM policy decision ③ execute defense
   ▼
CRE Simulator
   │  Compiles main.ts → WASM, executes along the (simulated) enclave path
   │  10 secrets injected via the secrets.yaml mapping (sequencing preference comes from public workflow config, not secrets.yaml)
   │  LLM decision → enforcePolicy hard checks → execution
   ▼
Result: SAFE (no action) or DEFENDED (defense executed)

What’s Next

Both case studies are done! Let’s wrap up the bootcamp and plan your next steps.

Wrap-Up: End-to-End Recap and Next Steps

Two Days in Review

Day 1: CRE + Confidential Workflow Fundamentals

  • CRE core concepts: Workflows / Triggers / Capabilities / DONs / consensus
  • Confidential Workflows: TEEs (AWS Nitro), enclaves, the Vault DON, attestation
  • The confidentiality boundary: what’s protected (secrets, in-enclave data, confidential HTTP) and what isn’t (code, triggers, chain interactions)
  • Case Study 1: AI Audit Firewall — dual-LLM confidential audit + conservative verdicts + optional onchain delivery

Day 2: Automated Liquidation Protection Hands-On

  • Finance concepts: overcollateralization, LTV, liquidation thresholds, health factors, liquidation mechanics and defenses
  • Case Study 2: Automated Liquidation Protection — policy-as-secrets, LLM decisions + deterministic guardrails, defense execution

Core Patterns Cheat Sheet

The Confidential Workflow development patterns distilled from these two case studies can be reused directly in your own projects:

PatternAPIPurpose
Confidential handlerhandlerInTee(trigger, callback, [{ tee: "nitro", regions }])Run the callback inside an enclave
In-enclave secret fetchruntime.getSecret({ id })Secrets released by the Vault DON directly into the enclave
Confidential HTTPclient.sendRequest(runtime, {...})URL / headers / body stay confidential from nodes
Capability restrictionspreHook: (cfg) => buildRestrictions(cfg)Least-privilege allowlist (capability call counts, secret list)
Crossing back to the DONruntime.usingTheDons()Operations needing consensus (e.g., signed onchain reports)
Policy-as-secretsPut thresholds/parameters in the Vault DONPrevent your strategy from being predicted or front-run
Deterministic backstopValidate LLM output with rulesCapital safety red lines don’t depend on model reliability

Decision Framework: Does Your Project Need a Confidential Workflow?

Does your workflow handle any of the following?
├─ Credentials with trading / withdrawal / spending permissions?
├─ Risk thresholds, strategy parameters, or configs that must not be public?
├─ Sensitive data processed by proprietary models or scoring logic?
└─ Payment data, PII, or other compliance-sensitive information?
        │
   Any "yes" → use handlerInTee to move that part into an enclave
   All "no"   → a regular workflow with standard secrets is enough

Keep Learning

Go Deeper on Confidential Computing

More Templates

Deploying to Production

Local simulation is fully open; deploying a Confidential Workflow to run on a DON requires:

  1. Request Early Access deployment access: cre account access or visit app.chain.link/cre/request-access
  2. Request Confidential Workflows access (Private Beta): see Requesting Confidential Workflows Access
  3. Follow the deployment guide

⚠️ Production reminder: remove or gate debug logs inside enclave logic before deploying — anything logged from within a Confidential Workflow could leak the data the enclave is meant to protect.

Stay Connected

Congratulations!

🎉 Congratulations on completing the CRE Confidential Bootcamp! 🎉

We can’t wait to see the confidential computing applications you build.