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
- Node.js v20 or later - Download here
- Bun v1.3 or later - Download here
- CRE CLI - Installation guide
- CRE account - Sign up at cre.chain.link and complete
cre login(see CRE CLI Quick Setup) - Deployment access - Fill in this form with your own information to get deployment access
- Git - Download here
Optional (only needed for the onchain write exercise)
- Add the Ethereum Sepolia network to your wallet - Add it here
- Get Sepolia ETH from a faucet - Chainlink Faucet
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.
Recommended
- 📚 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 Study | Path |
|---|---|
| AI Audit Firewall | cre-templates/starter-templates/confidential-workflows/ai-audit-firewall |
| Automated Liquidation Protection | cre-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 Kong Developer Relations Engineer, Chainlink Labs X (Twitter): @frank_chainlink LinkedIn: Frank Kong |
![]() | Darby Martinez Developer Relations Engineer, Chainlink Labs X (Twitter): @darbease LinkedIn: Darby Martinez |
![]() | Solange 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 Study | One-liner | What confidentiality protects |
|---|---|---|
| AI Audit Firewall | Before a transaction executes, two LLMs audit the target contracts inside an enclave and produce an ALLOW / DENY / MANUAL_REVIEW verdict, optionally written onchain | Scanner and LLM API credentials, plus the audit process data |
| Automated Liquidation Protection | Continuously monitors a lending position’s health and automatically adds collateral or repays debt before liquidation happens | Exchange credentials, risk thresholds, and defense strategy parameters |
Both case studies come from the official CRE templates repo: cre-templates.
Stay Connected
Join the Chainlink Community
- Subscribe to the Chainlink developer newsletter
- Follow Chainlink on X (Twitter)
- Follow Chainlink on LinkedIn
- Subscribe to the official Chainlink YouTube channel
- Join the Discord
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
- Visit cre.chain.link
- Create an account or log in
- 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
What Can You Do Now?
With CRE set up, you can:
- Create a new CRE project: Run
cre initto 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:
| Trigger | When it fires | Example |
|---|---|---|
| CRON | On a schedule | “Check position health every 5 minutes” |
| HTTP | When an HTTP request arrives | “Start an audit when the API is called” |
| Log | When 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:
| Capability | What it does |
|---|---|
| HTTP | Make HTTP requests to external APIs |
| EVM Read | Read data from smart contracts |
| EVM Write | Write data to smart contracts |
| Confidential HTTP | Make 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:
- Independently execute your workflow
- Compare their results
- Reach consensus using a Byzantine Fault Tolerant (BFT) protocol
- 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
| Concept | One-liner |
|---|---|
| Workflow | Your automation logic, compiled to WASM |
| Trigger | The event that starts execution (CRON, HTTP, Log) |
| Callback | The function containing your business logic |
| Capability | A microservice performing a specific task (HTTP, EVM Read/Write, Confidential HTTP) |
| DON | A set of network nodes executing under consensus |
| Consensus | The 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).
How Chainlink Implements Confidential Execution
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 enclave | Workflow 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 enclave | Your 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 enclave | Regular DON execution is usually fine |
|---|---|
| Exchange credentials with trading/withdrawal access, institutional custody credentials | API keys for publicly available data (weather, block explorers, public market data) |
| OAuth client secrets, KMS keys, payment processor, banking, and payment network credentials | Public wallet addresses |
| LLM API keys with spending limits, proprietary data provider credentials | Other 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 case | What 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 rebalancing | Allocation policy and trade sizing stay confidential, so the rebalance can’t be anticipated and traded against |
| Automated trading | A private strategy can’t be copied or traded against, even though the resulting transactions are onchain |
| Automated payment orchestration | Routing logic and account details stay confidential from node operators |
| Proprietary data computation | Both the data you can’t expose and the computation over it stay confidential |
Key Takeaways
| Concept | One-liner |
|---|---|
| TEE / Enclave | A hardware-isolated execution environment — even the machine’s operator can’t see the computation inside |
| Confidential Workflow | A standard CRE workflow that designates part of its logic to run inside an enclave |
| TeeRuntime | The runtime given to callbacks executing inside the enclave |
| Vault DON | The DON that releases secrets directly into the attested enclave |
| Attestation | Proof that the enclave is running the expected workflow logic, verified by DON consensus |
handlerInTee | The 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:
- Takes the context of the proposed transaction (target token contract, protocol contract, calldata, etc.)
- Fetches both contracts’ source code, ABI, and verification status through a chain scanner
- Runs an AI-powered security audit on the contracts, producing structured risk signals
- Issues a firewall verdict based on aggregate risk: allow, block, or route for manual review
- 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 asset | Impact of leakage |
|---|---|
| 🔑 Scanner API credentials | Third parties could abuse your paid scanner quota or spoof scan results |
| 🔑 Both LLM API credentials | Keys with spending limits get stolen — direct financial loss |
| 📄 Fetched contract source code and audit intermediates | The audit process gets observed; attackers can craft contracts that specifically evade detection |
| 🧠 Audit prompts and evaluation process | Once 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:
- Either model flags any risk signal → DENY
- Otherwise, if either model recommends review, has confidence below 0.7, or the two models disagree → MANUAL_REVIEW
- 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 ID | Purpose |
|---|---|
scanner_api_key | Access the contract scanner (fetch source/ABI, verify credentials) |
primary_llm_api_key | Call the first audit model |
secondary_llm_api_key | Call 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 endpoint | Simulated role |
|---|---|
GET /audit-firewall/transaction-proposal | Source of transaction proposals |
GET /audit-firewall/scanner/contracts/:address | Contract scanner (like an Etherscan-style service) |
GET /audit-firewall/scanner/credentials/verify | Scanner credential validation |
POST /audit-firewall/v1/analysis/primary | The first LLM audit model |
POST /audit-firewall/v1/analysis/secondary | The second LLM audit model |
POST /audit-firewall/audit-log | Audit logging service |
POST /audit-firewall/firewall-action | Firewall 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>toTeeRuntime<Config>— which exposes in-enclave capabilities (such as the enclave release path ofgetSecrets, 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 separategetSecretcalls. - Secret IDs are not hardcoded; they’re injected via the
secrets_idsfield inconfig.staging.json. In local simulation, the actual secret values come from the.envfile at the project root (mapped throughsecrets.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;
};
| Field | Meaning |
|---|---|
chain_selector / chain_name | The target chain |
tx_hash | The proposal’s transaction hash |
from_address / signer | The initiator and the signer |
token_contract_address | The token contract involved |
protocol_contract_address | The protocol contract to interact with |
calldata / value_wei | The call data and the amount of native token being transferred |
requested_action | The 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 condition | Result | Why it’s conservative |
|---|---|---|
Any risk flag from either model is true (mergeFlags is pure OR logic) | DENY | No consensus needed — one model flagging a problem is enough |
Either model recommends review | MANUAL_REVIEW | If one auditor isn’t sure, there’s no automatic pass |
Either model has confidence < 0.7 | MANUAL_REVIEW | Even an “allow” conclusion doesn’t count if the model isn’t confident |
| The two models’ recommendations disagree | MANUAL_REVIEW | Disagreement 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.solreceives the report: it extendsReceiverTemplateand decodes(uint8, uint8, uint64)in_processReport, recording theVerdict(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:
| Risk | Regular workflow | Confidential Workflow |
|---|---|---|
| LLM / scanner API keys | Plaintext passes through DON node memory, visible to node operators | Released by the Vault DON directly into the enclave; invisible to nodes |
| Contract source code and audit intermediates | Visible to nodes | Processed only in enclave memory |
| Audit requests (URL / headers / body) | Visible to nodes | Confidential HTTP; invisible to nodes |
| Audit criteria exploited to evade detection | Possible | The 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 thesecrets.yamlmapping. 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.
Step 4: Typecheck and Unit Tests (Optional but Recommended)
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 line | Meaning |
|---|---|
audit-firewall-getsecrets-ok | All 3 secrets were successfully fetched inside the (simulated) enclave via a single batched getSecrets call |
audit-firewall-scanner-credentials-ok | Scanner credentials validated, with contracts:read and verification:read scopes |
audit-firewall-onchain-report-start | Started 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_urlinconfig.staging.jsonfor 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
- Deploy
contracts/AuditFirewallConsumer.solto Sepolia (the constructor argument is the CRE Forwarder address) - Fill the deployed address into
evms[0].consumer_addressinconfig.staging.json - Set
CRE_ETH_PRIVATE_KEYin.env - 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
0x15fC6ae953E024d975e77382eEeC56A9101f9F88as 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 batchedgetSecrets, confidential HTTP, capability restrictions, andusingTheDons() - ✅ 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:
- clone the repo
- configure the environment variables
- simulate
- deploy
What This Workflow Does
This template is the minimal end-to-end shape of a Confidential Workflow, in four steps:
| Step | What it demonstrates | API |
|---|---|---|
| 1 | Register a handler whose callback runs inside a secure enclave | cre.handlerInTee(trigger, fn, tees) |
| 2 | Fetch a secret released by the Vault DON, inside the enclave | runtime.getSecret({ id }) |
| 3 | Make an HTTP call from inside the enclave (request and response stay confidential) | HTTPClient.sendRequest(teeRuntime, req) |
| 4 | Cross back to the Workflow DON for anything needing consensus | runtime.usingTheDons() |
Concretely, on every CRON tick the workflow:
- Hands the triggered request to an enclave (AWS Nitro,
us-west-2) instead of executing the callback on Workflow DON nodes - Fetches the
API_TOKENsecret — released by the Vault DON directly into the attested enclave - Calls the configured URL from inside the enclave with the secret in the
Authorizationheader - Scores the confidential response against
scoreThreshold→ verdictAPPROVE/REJECT - 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
.envaccording 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: truemeans the Vault DON secret was fetched inside the enclave and arrived in the outbound request’sAuthorizationheader.- The verdict can flip between
APPROVEandREJECTfrom run to run — the score derives from the live response body, and the echo endpoint includes a per-request trace ID. - Lower
scoreThresholdinmy-workflow/config.staging.jsonto seeAPPROVEconsistently.
Deploy the Workflow
Deployment takes 3 steps:
- add the secret to the Vault DON
- deploy
- 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_TOKENis set in your.envbefore 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 everyruntime.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
| Concept | One-liner |
|---|---|
cre workflow simulate | Runs the workflow locally along a simulated enclave path — secrets come from .env |
cre secrets create | Stores secrets in the Vault DON — required before deploying |
cre workflow deploy | Compiles, uploads, and registers the workflow on a DON |
| Private registry | Chainlink-hosted registry authorized by your CRE login — no wallet or gas |
| Vault DON | Releases 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 value | Position status |
|---|---|
| HF > 1 | Safe, sufficiently collateralized |
| HF = 1 | The liquidation line! Can be liquidated on arrival |
| HF < 1 | Undercollateralized, 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:
- Repay part of your debt on your behalf (say, 50%)
- 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)
| Action | Description |
|---|---|
add_collateral | Add collateral directly using stablecoin reserves |
bridge_and_add_collateral | Bridge assets from another chain, then add |
swap_reserve_to_collateral | Swap reserves into the collateral asset, then deposit |
Approach 2: Reduce debt (shrink the denominator)
| Action | Description |
|---|---|
repay_with_reserves | Repay part of the debt directly with reserves |
swap_reserve_to_borrowed_and_repay | Swap reserves into the borrowed asset, then repay |
partial_debt_repayment | Repay a percentage (say, 18%) of the debt |
full_debt_repayment | Repay 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:
- Automation: 24/7 monitoring + automatic execution → CRE Workflow
- Confidentiality: thresholds, strategy, and credentials hidden from node operators → Confidential Workflow
Key Takeaways
| Concept | One-liner |
|---|---|
| Overcollateralization | Deposit collateral worth more than what you borrow |
| LTV | The debt / collateral ratio |
| Liquidation threshold | LTV crossing it → can be liquidated |
| Health Factor (HF) | (collateral × liquidation threshold) / debt; < 1 means danger |
| Liquidation | A liquidator repays your debt and seizes discounted collateral + a bonus |
| Defense | Add collateral (numerator ↑) or repay debt (denominator ↓) |
| Confidential defense | Keep 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:
- 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)
- Inside a confidential environment, computes a risk score using your private policy parameters, and asks an LLM policy engine for a defense decision
- Validates the decision against hard policy constraints (deployment caps, reserve floors, …)
- 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 asset | Impact of leakage |
|---|---|
| 🔑 Exchange API credentials | Credentials that can query your account balances and reserves get stolen |
| 🔑 LLM API credentials | Keys 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 venues | Your 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:
| Type | Action | Category |
|---|---|---|
add_collateral | Add collateral directly with reserves | Collateral side |
bridge_and_add_collateral | Bridge assets cross-chain, then add | Collateral side |
swap_reserve_to_collateral | Swap reserves into the collateral asset, then deposit | Collateral side |
repay_with_reserves | Repay debt directly with reserves | Debt side |
swap_reserve_to_borrowed_and_repay | Swap reserves into the borrowed asset, then repay | Debt side |
partial_debt_repayment | Repay a percentage of the debt | Debt side |
full_debt_repayment | Repay the debt in full | Debt 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 ID | Type | Purpose |
|---|---|---|
exchange_api_key | Credential | Access account data (risk snapshot, execute defense) |
openai_api_key | Credential | Call the LLM policy engine |
liquidation_liquidation_warning_action_threshold | Policy | Liquidation proximity warning line (default 18%) |
liquidation_minimum_health_factor | Policy | Minimum health factor (default 1.25) |
liquidation_target_health_factor | Policy | Target health factor (default 1.5) |
liquidation_maximum_stablecoin_reserve_deployment | Policy | Max reserve deployment per execution, $ (default 5000) |
liquidation_minimum_stablecoin_reserve_balance | Policy | Minimum reserve balance, $ (default 2000) |
liquidation_maximum_collateral_allocation | Policy | Collateral allocation cap, % (default 80) |
liquidation_maximum_partial_debt_repayment | Policy | Partial repayment cap, % (default 40) |
liquidation_preferred_venues | Policy | Preferred 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 endpoint | Simulated role |
|---|---|
GET /liquidation/risk-state | Exchange/account data source (risk snapshot) |
POST /liquidation/v1/responses | OpenAI-style LLM policy engine |
POST /liquidation/execute-defense | Defense 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
getSecretscall resolves all 10 secrets (2 credentials + 8 policy-parameter secrets) in a single round trip to the Vault DON, instead of 10 individualgetSecretcalls. - 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_preferenceis the one exception: it’s read directly fromruntime.config, not fromgetSecrets— it’s plain workflow configuration, not Vault DON-protected.- These values are then assembled into the in-memory
Policyobject 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
| Dimension | Case 1: AI Audit Firewall | Case 2: Liquidation Protection |
|---|---|---|
| Confidential assets | Scanner + LLM credentials | Credentials + 8 policy-parameter secrets (sequencing preference is public config) |
| Decision-making | Dual LLMs + conservative merge rules | LLM decision + deterministic guardrails |
| Capability restrictions | preHook allowlist (HTTP ≤ 8, Report ≤ 1, secrets ≤ 3) | None declared (default limits) |
| Data leaving the enclave | Verdict code + risk mask (optionally onchain) | Only the defense action instructions (to the execution endpoint) |
| Number of LLMs | 2 (Primary / Secondary) | 1 policy engine |
Recap: Why Confidentiality Is Necessary
| Risk | Regular workflow | Confidential Workflow |
|---|---|---|
| Risk thresholds (when you act) | Visible to nodes → predictable and exploitable | Enter the enclave only, as secrets |
| Capital caps (how much you can deploy) | Visible to nodes → exposes your defensive boundary | Enter the enclave only, as secrets |
| Execution preferences (how you act) | Visible to nodes → front-runnable | Read from public workflow config, not a Vault secret — an intentional exception; capital caps and thresholds still stay enclave-only |
| Exchange / LLM credentials | Plaintext passes through node memory | Released by the Vault DON directly into the enclave |
| Risk snapshot (your position and reserves) | Visible to nodes | Confidential 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 inconfig.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.envat all — it’s set directly inconfig.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.
Step 4: Typecheck and Unit Tests (Optional but Recommended)
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 line | Meaning |
|---|---|
liquidation-getsecrets-ok | All 10 secrets (2 credentials + 8 policy-parameter secrets) fetched inside the enclave; the sequencing preference is read separately from public config |
liquidation-defense-executed | The 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_repayment18% ≤ cap 40% ✅ → converted amount $32,000 × 18% = $5,760- Ordered per
collateral-first:add_collateralfirst, thenpartial_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:
| Pattern | API | Purpose |
|---|---|---|
| Confidential handler | handlerInTee(trigger, callback, [{ tee: "nitro", regions }]) | Run the callback inside an enclave |
| In-enclave secret fetch | runtime.getSecret({ id }) | Secrets released by the Vault DON directly into the enclave |
| Confidential HTTP | client.sendRequest(runtime, {...}) | URL / headers / body stay confidential from nodes |
| Capability restrictions | preHook: (cfg) => buildRestrictions(cfg) | Least-privilege allowlist (capability call counts, secret list) |
| Crossing back to the DON | runtime.usingTheDons() | Operations needing consensus (e.g., signed onchain reports) |
| Policy-as-secrets | Put thresholds/parameters in the Vault DON | Prevent your strategy from being predicted or front-run |
| Deterministic backstop | Validate LLM output with rules | Capital 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
- Confidential Workflows concepts (official docs)
- Making a Workflow Confidential (step-by-step guide)
- Confidential HTTP Client
- Secrets management
More Templates
- Automated Portfolio Rebalancing — the third confidential template in this series, great self-study material
- CRE Templates Hub — all official templates
Deploying to Production
Local simulation is fully open; deploying a Confidential Workflow to run on a DON requires:
- Request Early Access deployment access:
cre account accessor visit app.chain.link/cre/request-access - Request Confidential Workflows access (Private Beta): see Requesting Confidential Workflows Access
- 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
- Join the Chainlink Discord — the most active channel for technical Q&A
- Subscribe to the developer newsletter
- Follow Chainlink on X
Congratulations!
🎉 Congratulations on completing the CRE Confidential Bootcamp! 🎉
We can’t wait to see the confidential computing applications you build.


