完整 Workflow:把它们接在一起
是时候把所学组合成一个可运行的完整结算 workflow 了!
完整流程
SettlementRequested Event
│
▼
Log Trigger
│
▼
┌────────────────────┐
│ Step 1: Decode │
│ Event data │
└────────┬───────────┘
│
▼
┌────────────────────┐
│ Step 2: EVM Read │
│ Get market details │
└────────┬───────────┘
│
▼
┌────────────────────┐
│ Step 3: HTTP │
│ Query DeepSeek AI │
└────────┬───────────┘
│
▼
┌────────────────────┐
│ Step 4: EVM Write │
│ Submit settlement │
└────────┬───────────┘
│
▼
Return txHash
完整的 logCallback.ts
用下面的完整结算流程更新 my-workflow/logCallback.ts:
// prediction-market/my-workflow/logCallback.ts
import {
cre,
type Runtime,
type EVMLog,
getNetwork,
bytesToHex,
hexToBase64,
TxStatus,
encodeCallMsg,
} from "@chainlink/cre-sdk";
import {
decodeEventLog,
parseAbi,
encodeAbiParameters,
parseAbiParameters,
encodeFunctionData,
decodeFunctionResult,
zeroAddress,
} from "viem";
import { askDeepSeek } from "./deepseek";
// Inline types
type Config = {
deepseekModel: string;
evms: Array<{
marketAddress: string;
chainSelectorName: string;
gasLimit: string;
}>;
};
interface Market {
creator: string;
createdAt: bigint;
settledAt: bigint;
settled: boolean;
confidence: number;
outcome: number; // 0 = Yes, 1 = No
totalYesPool: bigint;
totalNoPool: bigint;
question: string;
}
interface AIResult {
result: "YES" | "NO" | "INCONCLUSIVE";
confidence: number; // 0-10000
}
// ===========================
// Contract ABIs
// ===========================
/** ABI for the SettlementRequested event */
const EVENT_ABI = parseAbi([
"event SettlementRequested(uint256 indexed marketId, string question)",
]);
/** ABI for reading market data */
const GET_MARKET_ABI = [
{
name: "getMarket",
type: "function",
stateMutability: "view",
inputs: [{ name: "marketId", type: "uint256" }],
outputs: [
{
name: "",
type: "tuple",
components: [
{ name: "creator", type: "address" },
{ name: "createdAt", type: "uint48" },
{ name: "settledAt", type: "uint48" },
{ name: "settled", type: "bool" },
{ name: "confidence", type: "uint16" },
{ name: "outcome", type: "uint8" },
{ name: "totalYesPool", type: "uint256" },
{ name: "totalNoPool", type: "uint256" },
{ name: "question", type: "string" },
],
},
],
},
] as const;
/** ABI parameters for settlement report (outcome is uint8 for Prediction enum) */
const SETTLEMENT_PARAMS = parseAbiParameters("uint256 marketId, uint8 outcome, uint16 confidence");
// ===========================
// Log Trigger Handler
// ===========================
/**
* Handles Log Trigger events for settling prediction markets.
*
* Flow:
* 1. Decode the SettlementRequested event
* 2. Read market details from the contract (EVM Read)
* 3. Query DeepSeek AI for the outcome (HTTP)
* 4. Write the settlement report to the contract (EVM Write)
*
* @param runtime - CRE runtime with config and capabilities
* @param log - The EVM log event data
* @returns Success message with transaction hash
*/
export function onLogTrigger(runtime: Runtime<Config>, log: EVMLog): string {
runtime.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
runtime.log("CRE Workflow: Log Trigger - Settle Market");
runtime.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
try {
// ─────────────────────────────────────────────────────────────
// Step 1: Decode the event log
// ─────────────────────────────────────────────────────────────
const topics = log.topics.map((t: Uint8Array) => bytesToHex(t)) as [
`0x${string}`,
...`0x${string}`[]
];
const data = bytesToHex(log.data);
const decodedLog = decodeEventLog({ abi: EVENT_ABI, data, topics });
const marketId = decodedLog.args.marketId as bigint;
const question = decodedLog.args.question as string;
runtime.log(`[Step 1] Settlement requested for Market #${marketId}`);
runtime.log(`[Step 1] Question: "${question}"`);
// ─────────────────────────────────────────────────────────────
// Step 2: Read market details from contract (EVM Read)
// ─────────────────────────────────────────────────────────────
runtime.log("[Step 2] Reading market details from contract...");
const evmConfig = runtime.config.evms[0];
const network = getNetwork({
chainFamily: "evm",
chainSelectorName: evmConfig.chainSelectorName,
isTestnet: true,
});
if (!network) {
throw new Error(`Unknown chain: ${evmConfig.chainSelectorName}`);
}
const evmClient = new cre.capabilities.EVMClient(network.chainSelector.selector);
const callData = encodeFunctionData({
abi: GET_MARKET_ABI,
functionName: "getMarket",
args: [marketId],
});
const readResult = evmClient
.callContract(runtime, {
call: encodeCallMsg({
from: zeroAddress,
to: evmConfig.marketAddress as `0x${string}`,
data: callData,
})
})
.result();
const market = decodeFunctionResult({
abi: GET_MARKET_ABI as any,
functionName: "getMarket",
data: bytesToHex(readResult.data),
}) as Market;
runtime.log(`[Step 2] Market creator: ${market.creator}`);
runtime.log(`[Step 2] Already settled: ${market.settled}`);
runtime.log(`[Step 2] Yes Pool: ${market.totalYesPool}`);
runtime.log(`[Step 2] No Pool: ${market.totalNoPool}`);
if (market.settled) {
runtime.log("[Step 2] Market already settled, skipping...");
return "Market already settled";
}
// ─────────────────────────────────────────────────────────────
// Step 3: Query AI (HTTP)
// ─────────────────────────────────────────────────────────────
runtime.log("[Step 3] Querying DeepSeek AI...");
const aiResult = askDeepSeek(runtime, question);
// Extract JSON from response (AI may include prose before/after the JSON)
const jsonMatch = aiResult.aiResponse.match(/\{[\s\S]*"result"[\s\S]*"confidence"[\s\S]*\}/);
if (!jsonMatch) {
throw new Error(`Could not find JSON in AI response: ${aiResult.aiResponse}`);
}
const parsed = JSON.parse(jsonMatch[0]) as AIResult;
// Validate the result - only YES or NO can settle a market
if (!["YES", "NO"].includes(parsed.result)) {
throw new Error(`Cannot settle: AI returned ${parsed.result}. Only YES or NO can settle a market.`);
}
if (parsed.confidence < 0 || parsed.confidence > 10000) {
throw new Error(`Invalid confidence: ${parsed.confidence}`);
}
runtime.log(`[Step 3] AI Result: ${parsed.result}`);
runtime.log(`[Step 3] AI Confidence: ${parsed.confidence / 100}%`);
// Convert result string to Prediction enum value (0 = Yes, 1 = No)
const outcomeValue = parsed.result === "YES" ? 0 : 1;
// ─────────────────────────────────────────────────────────────
// Step 4: Write settlement report to contract (EVM Write)
// ─────────────────────────────────────────────────────────────
runtime.log("[Step 4] Generating settlement report...");
// Encode settlement data
const settlementData = encodeAbiParameters(SETTLEMENT_PARAMS, [
marketId,
outcomeValue,
parsed.confidence,
]);
// Prepend 0x01 prefix so contract routes to _settleMarket
const reportData = ("0x01" + settlementData.slice(2)) as `0x${string}`;
const reportResponse = runtime
.report({
encodedPayload: hexToBase64(reportData),
encoderName: "evm",
signingAlgo: "ecdsa",
hashingAlgo: "keccak256",
})
.result();
runtime.log(`[Step 4] Writing to contract: ${evmConfig.marketAddress}`);
const writeResult = evmClient
.writeReport(runtime, {
receiver: evmConfig.marketAddress,
report: reportResponse,
gasConfig: {
gasLimit: evmConfig.gasLimit,
},
})
.result();
if (writeResult.txStatus === TxStatus.SUCCESS) {
const txHash = bytesToHex(writeResult.txHash || new Uint8Array(32));
runtime.log(`[Step 4] ✓ Settlement successful: ${txHash}`);
runtime.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
return `Settled: ${txHash}`;
}
throw new Error(`Transaction failed: ${writeResult.txStatus}`);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
runtime.log(`[ERROR] ${msg}`);
runtime.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
throw err;
}
}
下注预测
在请求结算之前,我们先在市场上做一次预测。这样可以演示完整流程:用 ETH 下注、AI 结算、赢家领取份额。
# Predict YES on market #0 with 0.01 ETH
# Prediction enum: 0 = Yes, 1 = No
cast send $MARKET_ADDRESS \
"predict(uint256,uint8)" 0 0 \
--value 0.01ether \
--rpc-url "https://ethereum-sepolia-rpc.publicnode.com" \
--private-key $CRE_ETH_PRIVATE_KEY
然后可以再次查看市场详情:
cast call $MARKET_ADDRESS \
"getMarket(uint256) returns ((address,uint48,uint48,bool,uint16,uint8,uint256,uint256,string))" \
0 \
--rpc-url "https://ethereum-sepolia-rpc.publicnode.com"
也可以只查自己的预测:
export PREDICTOR=0xYOUR_WALLET_ADDRESS
cast call $MARKET_ADDRESS \
"getPrediction(uint256,address) returns ((uint256,uint8,bool))" \
0 $PREDICTOR \
--rpc-url "https://ethereum-sepolia-rpc.publicnode.com"
可以有多名参与者分别预测——有人选 YES,有人选 NO。CRE 完成市场结算后,赢家可以调用 claim() 领取总池中的份额!
结算市场
下面用 Log Trigger 执行完整结算流程。
步骤 1:请求结算
首先从智能合约触发 SettlementRequested 事件:
cast send $MARKET_ADDRESS \
"requestSettlement(uint256)" 0 \
--rpc-url "https://ethereum-sepolia-rpc.publicnode.com" \
--private-key $CRE_ETH_PRIVATE_KEY
**请保存交易哈希!**下一步会用到。
步骤 2:运行 Simulation
cre workflow simulate my-workflow --broadcast
步骤 3:选择 Log Trigger
🚀 Workflow simulation ready. Please select a trigger:
1. http-trigger@1.0.0-alpha Trigger
2. evm:ChainSelector:16015286601757825753@1.0.0 LogTrigger
Enter your choice (1-2): 2
步骤 4:输入交易信息
🔗 EVM Trigger Configuration:
Please provide the transaction hash and event index for the EVM log event.
Enter transaction hash (0x...):
粘贴步骤 1 中的交易哈希。
步骤 5:输入 Event Index
Enter event index (0-based): 0
输入0。
预期输出
[SIMULATION] Running trigger trigger=evm:ChainSelector:16015286601757825753@1.0.0
[USER LOG] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[USER LOG] CRE Workflow: Log Trigger - Settle Market
[USER LOG] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[USER LOG] [Step 1] Settlement requested for Market #0
[USER LOG] [Step 1] Question: "Will Argentina win the 2022 World Cup?"
[USER LOG] [Step 2] Reading market details from contract...
[USER LOG] [Step 2] Market creator: 0x...
[USER LOG] [Step 2] Already settled: false
[USER LOG] [Step 2] Yes Pool: 10000000000000000
[USER LOG] [Step 2] No Pool: 0
[USER LOG] [Step 3] Querying DeepSeek AI...
[USER LOG] [DeepSeek] Querying AI for market outcome...
[USER LOG] [DeepSeek] Response received: {"result":"YES","confidence":10000}
[USER LOG] [Step 3] AI Result: YES
[USER LOG] [Step 3] AI Confidence: 100%
[USER LOG] [Step 4] Generating settlement report...
[USER LOG] [Step 4] Writing to contract: 0x...
[USER LOG] [Step 4] ✓ Settlement successful: 0xabc123...
[USER LOG] ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Workflow Simulation Result:
"Settled: 0xabc123..."
[SIMULATION] Execution finished signal received
步骤 6:链上验证结算
cast call $MARKET_ADDRESS \
"getMarket(uint256) returns ((address,uint48,uint48,bool,uint16,uint8,uint256,uint256,string))" \
0 \
--rpc-url "https://ethereum-sepolia-rpc.publicnode.com"
你应能看到 settled: true 以及由 AI 判定的结果!
步骤 7:领取奖金
如果你预测的是获胜结果,可以领取池中属于你的部分:
cast send $MARKET_ADDRESS \
"claim(uint256)" 0 \
--rpc-url "https://ethereum-sepolia-rpc.publicnode.com" \
--private-key $CRE_ETH_PRIVATE_KEY
故障排查
Deepseek API 报错:402
如果你看到如下错误:
Capability 'consensus@1.0.0-alpha' method 'Simple' returned an error: failed to execute capability: [2]Unknown: DeepSeek API error: 402 - {"error":{"message":"Insufficient Balance","type":"unknown_error","param":null,"code":"invalid_request_error"}}
请在 Deepseek Platform) 控制台为你的 Deepseek API key 开通计费。海外需要绑定信用卡以启用计费,国内用户可以使用支付宝和微信支付,不必担心——完成本 bootcamp 需要的费用极低,只要 0.01 人民币。

🎉 大功告成!
**恭喜!**你已经用 CRE 搭建并跑通了一个完整的 AI 驱动预测市场!
快速回顾你完成的内容:
| 能力 | 你构建的内容 |
|---|---|
| HTTP Trigger | 通过 API 请求创建市场 |
| Log Trigger | 基于事件的结算自动化 |
| EVM Read | 从链上读取市场状态 |
| HTTP (AI) | 查询 DeepSeek AI 获取真实世界结果 |
| EVM Write | 经 DON 共识验证的链上写入 |
你的 workflow 现在可以:
- ✅ 通过 HTTP 按需创建市场
- ✅ 监听链上事件以接收结算请求
- ✅ 从智能合约读取市场数据
- ✅ 查询 AI 以判定真实世界结果
- ✅ 将经校验的结算写回链上
- ✅ 让赢家领取奖励
下一步
前往最后一章,查看完整的端到端演练以及 CRE 之路的后续方向!