All projects

Agents of Truth (Team Zugzwang)

AI agents pay per API call in USDC on Base Sepolia via x402, while creators' API keys stay private through Chainlink CRE's Confidential HTTP and Vault DON.

CRE & AI Privacy

What it is

Agents of Truth

What It Is
Agents of Truth is a hackathon project (Team Zugzwang, Convergence × Chainlink) that lets AI agents pay for API skills on a per-call basis using the x402 payment protocol, while keeping creator API keys completely private using Chainlink CRE's Confidential HTTP and Vault DON.
Think of it as: "Atomic Fast payments for AI agents, but trust-less and private."

The Problem It Solves
Three compounding problems exist in today's AI agent ecosystem:

  1. Platform lock-in. Skills, connectors, and MCPs are tied to specific platforms. An agent on one platform can't discover or pay for a skill built on another. There's no open, interoperable marketplace.
  2. API key exposure. When a skill creator wants to monetize an API (say, a proprietary data feed), they have to hand their API key to a middleware server — in plaintext, on someone else's infrastructure. One breach leaks everything.
  3. Subscription overkill. A creator charges $29/month because that's the smallest plan makes sense. But the agent only needed $0.005 worth of compute. There's no infrastructure for micro-transactions at the per-call level.

The Solution: Two Interlocking Pieces
Piece 1 — x402: Atomic Per-Call Payments
x402 is an open HTTP payment protocol built on EIP-3009 (signed USDC transfers). The flow works like this:

  1. Agent sends POST /v1/skill/analyze to a Skill API
  2. Server returns HTTP 402 Payment Required with a price — e.g. 0.001 USDC on Base
  3. Agent signs an EIP-3009 authorization payload (no on-chain tx needed yet) and retries the request with an X-PAYMENT header
  4. The Skill API forwards the proof to a Facilitator (e.g. Coinbase's CDP endpoint) via /verify
  5. Facilitator confirms validity, submits the USDC transfer to Base L2 (~2 second finality)
  6. Skill API returns 200 OK with the result
    On the server side, this is literally 3 lines:
    ts
    app.use(paymentMiddleware(WALLET, {
    "POST /v1/skill/analyze": { price: "$0.001", network: "base-sepolia" },
    }));
    No accounts. No subscriptions. The agent just pays per call.

Piece 2 — CRE Confidential HTTP: Private API Keys
This solves the creator-side problem: how does the skill use a private API key without exposing it?
The flow uses Chainlink CRE's Vault DON and Confidential HTTP:

  1. The creator's API key is threshold-encrypted via Chainlink DKG — split across N vault nodes such that no single node holds the full key
  2. When a verified x402 payment triggers the CRE workflow, the workflow calls getSecret("CREATOR_API_KEY")
  3. The Vault DON provides decryption shares to a Secure Enclave (TEE) only after verifying authorization and performing remote attestation
  4. Inside the enclave, shares are recombined into the full secret
  5. The ConfidentialHTTPClient makes the outbound API call from inside the enclave — the key is used but never leaves the enclave in plaintext
  6. The response comes back, the secret is immediately discarded (KEY = null)
  7. The result is returned — never appearing in logs, chain state, or any node's memory
    In the actual codebase (privacy/workflow/main.ts), this maps directly to:

ts
const verifyRes = confidentialClient.sendRequest(runtime, {
vaultDonSecrets: [{ key: req.secret_id, version: 1 }],
request: { url: req.resource_url, encryptOutput: true }
}).result();

The `encryptOutput: true` flag means even the API response is encrypted before it leaves the enclave — DON nodes facilitating consensus can verify *that* it ran correctly without ever seeing the payload.

End-to-End Flow
AI Agent
  → POST /skill/analyze
  ← 402 Payment Required (0.001 USDC)
  → Retry with signed EIP-3009 X-PAYMENT header
    → Facilitator verifies proof
    → USDC settles on Base L2 (~2s)
  → x402 gateway fires JWT-signed CRE trigger
    → Workflow DON activates (all nodes run main.wasm in parallel)
    → ConfidentialHTTPClient: Vault DON provides decryption shares → TEE enclave
    → Creator's API key used inside enclave, never exposed
    → Result written on-chain via EVMClient (BFT consensus)
    → Secret discarded
  ← 200 OK + result returned to agent

Why It's Technically Interesting
The two pieces reinforce each other in a non-obvious way. x402 solves access control without accounts — any agent with USDC can call any skill, no registration required, fully open and discoverable. CRE Confidential HTTP solves credential privacy without trust — the creator never has to hand their API key to anyone, not even Chainlink nodes, because threshold encryption means no single party ever assembles the full key outside a TEE. Together they create a marketplace where both sides — agent and creator — operate without trusting any centralized intermediary.

Together this ensures:
Private by design.
Auditable by default.
Paid per call.

How it Works

How It’s Built : https://agents-of-truth.com

  1. The Runtime Model: Trigger → Callback → On-Chain
    CRE uses a trigger-and-callback model. It has three parts: a Trigger (the "when" — an event source), a Callback (the "what" — your business logic function), and handler() which is the glue connecting the two. 
    In both workflows in this repo, this pattern is explicit:
    Execution workflow — triggered by an on-chain EVM log:
    ts
    handler(
    agentContract.logTriggerServiceRequested(),
    onLogTrigger
    )
    Privacy workflow — triggered by an HTTP call
    ts
    handler(http.trigger({}), onHttpTrigger)
    The handler() is not just a function pointer — it wires the trigger's output schema to the callback's input type at the SDK level, producing a typed, statically-verified workflow graph.

  2. Compilation Target: WASM via Javy
    Workflows written in TypeScript are compiled into WebAssembly (WASM) for execution by Chainlink nodes. 
     The toolchain here is cre-compile, which uses Javy — a Rust-based JS-to-WASM compiler from Shopify — as its backend.
    From the package.json:
    json
    "postinstall": "bun x cre-setup"


This `cre-setup` postinstall script downloads the **Javy plugin binary** for the current platform. Then `bun x cre-compile main.ts` produces `main.wasm`. This WASM binary is what gets deployed and executed on each DON node independently.

The constraint this introduces is visible in the privacy workflow docs:
> *"We use `.startsWith("http")` over `.url()` to maintain compatibility with the Javy WASM execution engine."*

Zod's `.url()` validator internally uses browser APIs not available in Javy's constrained WASM runtime. This is a real, practical WASM compatibility concern baked into the code.

---

3. Decentralized Execution: Workflow DON + Capability DONs

When a trigger fires, the **Workflow DON** orchestrates execution of your callback on **every node** in the network. Each execution is **independent and stateless**. For every capability you invoke, CRE handles having a dedicated **Capability DON** execute the task, reach consensus, and return the verified result. 

So the flow is:

  EVM Log Event / HTTP Request
        ↓
  Workflow DON (all nodes receive trigger)
        ↓
  Each node runs main.wasm independently
        ↓
  ConfidentialHTTPClient → Capability DON (consensus on HTTP response)
        ↓
  EVMClient.writeReport() → on-chain write with BFT consensus

4. ConfidentialHTTPClient — The Privacy Layer
This is the core of both workflows. In the execution main.ts:
ts
const confidentialClient = new ConfidentialHTTPClient();
const verifyRes = confidentialClient.sendRequest(runtime, {
  vaultDonSecrets: [],
  request: {
    url: runtime.config.facilitator_verify_url,
    method: "POST",
    bodyString: JSON.stringify({ paymentProof: event.paymentPayload }),
    encryptOutput: false
  }
}).result();
The ConfidentialHTTPClient makes requests from inside a secure enclave on the DON. The Vault DON securely stores secrets using threshold encryption via Chainlink DKG — cryptographically dividing key access among a quorum of node operators so no single node can decrypt alone. When a secret is needed, nodes provide decryption shares to the enclave only after performing authorization checks and remote attestations. The enclave recombines these shares, executes the workflow, then discards the secret immediately.

The encryptOutput: true flag in the privacy workflow's second request means the response body is encrypted before leaving the enclave — nodes can't read the API response data even as they're running the workflow. encryptOutput: false on the verification step is intentional: all nodes need to agree on the 200 OK status (consensus requires visibility into it).
The vaultDonSecrets array in the privacy workflow:
ts
const secrets = req.secret_id ? [{ key: req.secret_id, version: 1 }] : [];
This is the DKG secret retrieval mechanism. The key reference is passed, and the Vault DON provides decryption shares at runtime.

5. The x402 Payment Flow (Execution Workflow)
The execution workflow implements a three-phase x402 payment protocol:
Phase 1 — Proof Verification (against Coinbase's facilitator):
ts
url: runtime.config.facilitator_verify_url, // api.cdp.coinbase.com/platform/v2/x402/verify
bodyString: JSON.stringify({ paymentProof: event.paymentPayload }),
encryptOutput: false  // consensus needs to see the 200 OK
Phase 2 — Resource Execution (fetch the paid resource):
ts
url: event.serviceUrl,
multiHeaders: { "X-X402-Payment-Proof": { values: [event.paymentPayload] } }
Phase 3 — On-Chain Fulfillment (write result back):
ts
agentContract.writeReportFromFulfillService(runtime, event.agent, resultData)
The trigger itself is an EVM log — a ServiceRequested event emitted by the X402Agent.sol contract. The SDK's logTriggerServiceRequested() encodes topic filters using encodeEventTopics() from viem, converts them to base64 for the DON's internal message format via hexToBase64(), and returns an object with an adapt() method that decodes raw EVMLog bytes into the typed ServiceRequestedDecoded struct.

6. Type-Safe Contract Bindings (Code-Generated)
The contracts/evm/ts/generated/X402Agent.ts file is code-generated from the ABI. The ABI is:
json
[
  { "name": "ServiceRequested", "type": "event", "inputs": [...] },
  { "name": "fulfillService", "type": "function", "inputs": [...] }
]
The generator produces:
- X402AgentABI — the typed ABI constant (as const for full TypeScript inference)
- X402Agent class — wraps EVMClient with typed methods
- writeReportFromFulfillService() — calls encodeFunctionData() from viem, then runtime.report(prepareReportRequest(callData)) to get a DON-consensus report, then client.writeReport() to submit it on-chain
- logTriggerServiceRequested() — builds the logTrigger config and returns an adapted trigger with typed decode
- X402AgentMock — uses addContractMock() from @chainlink/cre-sdk/test for local simulation
Note: x402-agent.ts (lowercase, hyphenated) is a duplicate/older generated version. The active one imported in main.ts is X402Agent.ts (PascalCase).

7. Config Layering: Staging vs Production
The CLI target system resolves config at deploy time:
yaml
# workflow.yaml
staging-settings:
  user-workflow:
    workflow-name: "execution-workflow-staging"
  workflow-artifacts:
    config-path: "./config.staging.json"

production-settings:
  user-workflow:
    workflow-name: "execution-workflow-production"
  workflow-artifacts:
    config-path: "./config.production.json"
config.staging.json uses Coinbase's CDP platform endpoint; config.production.json uses a beeceptor echo URL (a mock for testing production deployment path). The Config type in main.ts is the shape validated at startup — runtime.config is strongly typed to it.
The project.yaml separately configures the RPC endpoints the DON nodes use to interact with the chain (Sepolia here), and secrets.yaml declares which Vault DON secret names the workflow is authorized to access.

8. Simulation: Local WASM Dry-Run
bash
cre workflow simulate . -T staging-settings --http-payload payload.json

Simulation compiles your workflow into WebAssembly and runs it on your local machine — but makes real calls to live APIs and public EVM blockchains. This gives you confidence the workflow will behave correctly when deployed to a DON.

The experimental-chains block in project.yaml tells the simulator which chain selector → RPC URL → forwarder contract mapping to use for simulating on-chain writes, since this chain isn't yet in the official CRE chain registry.


Summary Architecture

TypeScript (main.ts)
    ↓ bun x cre-compile
  main.wasm (Javy/WASM)
    ↓ cre workflow deploy
  Workflow DON (N nodes, each runs main.wasm)
    ├── Log/HTTP Trigger → fires callback
    ├── ConfidentialHTTPClient → Capability DON (TEE + BFT consensus on response)
    │       └── Vault DON (DKG threshold decryption for secrets)
    └── EVMClient.writeReport() → BFT-signed on-chain tx to X402Agent contract
Every HTTP call, every secret access, and every on-chain write goes through DON consensus — the workflow code itself runs identically across all nodes, and results are only committed when a threshold agrees.

Links

Created by

  • Hrishikesh Hundekari