Terminal3 ×
19 June 2026
Builder Workshop · Terminal3 Agent Dev Kit

Building
AI Agents

with Terminal3 ADK
Speaker
Shi Wei
Common Ground KLCC · Kuala Lumpur Terminal3 × Kracked Devs Builder Meetup
Foundations

What's an AI agent?

Not just a chatbot. An agent perceives input, decides what to do, and acts — powered by tools and memory.

Perceive
Input / request
Decide
Reason / plan
Act
Call tools · respond
Memory
Remembers context across steps
Tools
Reaches the outside world
Terminal3 ADK Workshop02 / 23
The Platform

Meet Terminal3

A decentralized confidential-computing network — and the Agent Dev Kit (ADK) we'll build on today. The headline: agents that run on hardware-isolated infrastructure, so they handle sensitive data without exposing it.

01

Confidential by design

Agents execute inside a Trusted Execution Environment (TEE) — data stays sealed, even from the host.

02

Wallet-native identity

Each agent has a real identity (a DID), authenticated by your wallet — no scattered API keys.

03

An SDK, not a maze

The ADK gives you the primitives — tools, data, deployment — in a single TypeScript + Rust workflow.

Terminal3 ADK Workshop03 / 23
The PlatformADK Architecture

Tools + TEE = privacy by design

Your agent, its tools, and its memory all run inside the TEE — hardware-encrypted and attested. Even the host cloud sees only ciphertext.

T3 TRUSTED EXECUTION ENVIRONMENT AI Agent LLM · REASON · DECIDE WALLET-AUTHENTICATED DID Storage KV STORE · AGENT STATE PERSISTS ACROSS CALLS NOT VISIBLE TO HOST READ / WRITE Tools HTTP · KV ACCESS · SECRETS REGISTERED IN ADK · RUN IN TEE TOOL CALL RESULT TEE OUTBOUND HTTP RESPONSE External APIs / SERVICES e.g. FLIGHT API User WALLET · DID AUTH REQUEST HARDWARE ENCRYPTED · CODE ATTESTED · HOST CANNOT READ AGENT STATE
Terminal3 ADK Workshop04 / 23
Why It's Worth It

The bounty challenge

Before we build — here's why it's worth it. This workshop runs alongside the Terminal3 Agent Dev Kit Bounty Challenge. What you learn today is your head start.

$5,000
Total prize pool
$2,000
Cash
$3,000
Google Cloud credits

→ Build a real agent on the ADK, submit it, and you're in the running. We'll cover exactly how near the end.

Terminal3 ADK Workshop05 / 23
Live Walkthrough

Today's demo: a flight agent

Your intro to Terminal3 — by running its own sample agent end to end. No new contract code today; we run the prebuilt sample and watch an agent call two contract tools.

TOOL 1 · OUTBOUND HTTP

search-offers

Asks Duffel for flight offers. Needs only the Duffel test token — no passenger data at all.

TOOL 2 · HTTP + PLACEHOLDERS

book-offer

Books one test offer. Passenger PII is filled by the host from your profile via {{placeholders}} — never inside the agent.

⚠️ Duffel TEST token only — never a live token github.com/Terminal-3/z-tenant-flight
Terminal3 ADK Workshop06 / 23
Live WalkthroughMental Model

The mental model

Four pieces. You don't invent a namespace — you authenticate, read your DID back, and the SDK builds the names for you.

01 · CREDENTIAL

T3N API key

Your login to the Terminal3 network (T3N).

02 · IDENTITY

Your DID

Returned when you authenticate — your identity and namespace owner.

03 · CONTRACT

Tenant contract

The Rust → WASM flight contract, registered under your namespace.

04 · INVOCATION

Agent call

An authenticated session calls the contract's functions.

# authenticate → read your DID → the SDK builds these names: z:<your_did_hex>:travel-contracts # your contract z:<your_did_hex>:secrets # private KV (Duffel token)
Terminal3 ADK Workshop07 / 23
Step 1 · 2Set Up & Build

Set up & build

  • Claim your T3N API key, DID & T3N test token from the claim page
  • Create a Duffel account at app.duffel.com/signup
  • Export your keys — use a Duffel test token
  • Build the sample contract to a WASM component
# 1 · env vars (repo root) $ export T3N_API_KEY="your_terminal3_api_key" $ export DUFFEL_API_KEY="duffel_test_..." # 2 · build Rust → WASM $ rustup target add wasm32-wasip2 $ cargo build --target wasm32-wasip2 --release # → target/.../z_tenant_flight.wasm
Claim T3N Test Tokens

Note ·  for a first demo, the same T3N key can act as user, tenant admin, and agent.

Live Walkthrough · Build → Register → Authorize → Profile → Invoke08 / 23
Step 3Register

Register the contract

One setup script does it all: authenticate, register the contract, create a private KV map, and seed your Duffel token. Run it once.

REGISTER

Publish the WASM

Registered under z:<did>:travel-contracts — now agents can call it.

SECRETS MAP

Private KV store

Create z:<did>:secrets — readable & writable only by the contract.

SEED

Stash the token

Write duffel_api_key. The contract reads it at runtime — the agent never sees it.

$ node scripts/setup-flight-demo.mjs # full script on the next slide →
Live Walkthrough · Build → Register → Authorize → Profile → Invoke09 / 23
Step 3Script

setup-flight-demo.mjs

register · create secrets map · seed Duffel token
import { readFile } from "fs/promises";
import {
  T3nClient,
  TenantClient,
  setEnvironment,
  loadWasmComponent,
  eth_get_address,
  metamask_sign,
  createEthAuthInput,
  getNodeUrl,
} from "@terminal3/t3n-sdk";

setEnvironment("testnet");

function errorMessage(err) {
  return err instanceof Error ? err.message : String(err);
}

function isMapAlreadyExistsError(err) {
  const normalized = errorMessage(err).toLowerCase();
  return normalized.includes("map already exists") || normalized.includes("mapalreadyexists");
}

function isContractVersionAlreadyRegisteredError(err) {
  const normalized = errorMessage(err).toLowerCase();
  return (
    normalized.includes("contract version invalid") &&
    normalized.includes("is not higher than current version")
  );
}

function buildSecretMapAccess(contractId) {
  return {
    writers: { only: [contractId] },
    readers: { only: [contractId] },
  };
}

const T3N_API_KEY = process.env.T3N_API_KEY;
const DUFFEL_API_KEY = process.env.DUFFEL_API_KEY;

if (!T3N_API_KEY) throw new Error("Missing T3N_API_KEY");
if (!DUFFEL_API_KEY) throw new Error("Missing DUFFEL_API_KEY");
if (!DUFFEL_API_KEY.startsWith("duffel_test_")) {
  throw new Error("Use a Duffel test token for this demo, not a live token");
}

const CONTRACT_TAIL = "travel-contracts";
const CONTRACT_VERSION = process.env.CONTRACT_VERSION ?? "0.1.0";
const WASM_PATH = "target/wasm32-wasip2/release/z_tenant_flight.wasm";

const wasmComponent = await loadWasmComponent();
const address = eth_get_address(T3N_API_KEY);

const t3n = new T3nClient({
  wasmComponent,
  handlers: {
    EthSign: metamask_sign(address, undefined, T3N_API_KEY),
  },
});

console.log("Authenticating to T3N testnet...");
await t3n.handshake();
const did = await t3n.authenticate(createEthAuthInput(address));
const tenantDid = did.value;

console.log("Authenticated DID:", tenantDid);

const tenant = new TenantClient({
  t3n,
  baseUrl: getNodeUrl(),
  tenantDid,
});

console.log("Checking tenant...");
console.log(await tenant.tenant.me());

console.log("Reading WASM:", WASM_PATH);
const wasm = await readFile(WASM_PATH);

console.log("Registering contract...");
let registered;
try {
  registered = await tenant.contracts.register({
    tail: CONTRACT_TAIL,
    version: CONTRACT_VERSION,
    wasm,
  });
} catch (err) {
  if (isContractVersionAlreadyRegisteredError(err)) {
    throw new Error(
      `Contract ${CONTRACT_TAIL}@${CONTRACT_VERSION} is already registered. Rerun with a higher version, for example: CONTRACT_VERSION=0.1.3 node scripts/setup-flight-demo.mjs`,
      { cause: err },
    );
  }
  throw err;
}

const contractId = registered.contract_id;
const tenantId = tenantDid.slice("did:t3n:".length);
const tenantScript = `z:${tenantId}:${CONTRACT_TAIL}`;

console.log("Registered contract:", tenantScript);
console.log("Contract ID:", contractId);

console.log("Creating secrets map...");
const secretMapAccess = buildSecretMapAccess(contractId);
try {
  await tenant.maps.create({
    tail: "secrets",
    visibility: "private",
    ...secretMapAccess,
  });
  console.log("Created secrets map");
} catch (err) {
  if (isMapAlreadyExistsError(err)) {
    console.log("Secrets map already exists; updating access grants");
    await tenant.maps.update("secrets", secretMapAccess);
  } else {
    throw err;
  }
}

console.log("Seeding Duffel test token...");
await tenant.executeControl("map-entry-set", {
  map_name: tenant.canonicalName("secrets"),
  key: "duffel_api_key",
  value: DUFFEL_API_KEY,
});

console.log("Done.");
console.log({
  tenantDid,
  tenantScript,
  contractId,
  contractVersion: CONTRACT_VERSION,
});
Live Walkthrough · Build → Register → Authorize → Profile → Invoke10 / 23
Step 4Authorize

Authorize the agent

Before the agent can make outbound calls, the user must grant it permission. In beginner mode the user and agent are the same DID — a self-grant.

FUNCTIONS

search-offers · book-offer

Whitelist exactly the contract functions the agent may call.

EGRESS

allowedHosts

Permit api.duffel.com — the only host this agent can reach.

WHY

No grant, no call

Skip this and Duffel calls fail with an egress / permission error.

$ export TENANT_SCRIPT="z:<your_did_hex>:travel-contracts" $ node scripts/authorize-agent.mjs # full script next →
Live Walkthrough · Build → Register → Authorize → Profile → Invoke11 / 23
Step 4Script

authorize-agent.mjs

self-grant search-offers · book-offer · api.duffel.com
import {
  T3nClient, setEnvironment,
  loadWasmComponent, eth_get_address,
  metamask_sign, createEthAuthInput,
  getNodeUrl, getScriptVersion,
} from "@terminal3/t3n-sdk";

setEnvironment("testnet");

const T3N_API_KEY = process.env.T3N_API_KEY;
const TENANT_SCRIPT = process.env.TENANT_SCRIPT;

if (!T3N_API_KEY)
  throw new Error("Missing T3N_API_KEY");
if (!TENANT_SCRIPT)
  throw new Error("Missing TENANT_SCRIPT");

const wasmComponent = await loadWasmComponent();
const address = eth_get_address(T3N_API_KEY);

const userClient = new T3nClient({
  wasmComponent,
  handlers: {
    EthSign: metamask_sign(
      address, undefined, T3N_API_KEY),
  },
});

await userClient.handshake();
const did = await userClient.authenticate(
  createEthAuthInput(address));
const userDid = did.value;

// Beginner mode: user and agent
// are the same DID.
const agentDid = userDid;

const userContractVersion =
  await getScriptVersion(
    getNodeUrl(), "tee:user/contracts");
const scriptVersion =
  await getScriptVersion(
    getNodeUrl(), TENANT_SCRIPT);

console.log("Authorizing agent...");
await userClient.execute({
  script_name: "tee:user/contracts",
  script_version: userContractVersion,
  function_name: "agent-auth-update",
  input: {
    agents: [
      {
        agentDid,
        scripts: [
          {
            scriptName: TENANT_SCRIPT,
            versionReq: scriptVersion,
            functions: [
              "search-offers",
              "book-offer",
            ],
            allowedHosts: ["api.duffel.com"],
          },
        ],
      },
    ],
  },
});

console.log("Authorized agent:", agentDid);
console.log("For contract:", TENANT_SCRIPT);
Live Walkthrough · Build → Register → Authorize → Profile → Invoke12 / 23
Step 5Profile

Set your profile

search-offers needs no PII. book-offer does — but the contract never receives it as input. It uses profile placeholders the host resolves inside the TEE.

# resolved by the host, inside the TEE {{profile.first_name}} {{profile.last_name}} {{profile.date_of_birth}} {{profile.gender}} {{profile.verified_contacts.email.value}}
  • Verify your email — an OTP is sent and confirmed
  • Store profile fields name, DOB, gender, email
  • Plaintext PII never enters the contract's WASM
$ node scripts/update-flight-profile.mjs # full script next →
Live Walkthrough · Build → Register → Authorize → Profile → Invoke13 / 23
Step 5Script

update-flight-profile.mjs

verify email via OTP · store profile fields
import { createInterface } from "readline/promises";
import { stdin as input, stdout as output }
  from "process";
import {
  T3nClient, setEnvironment,
  loadWasmComponent, eth_get_address,
  metamask_sign, createEthAuthInput,
} from "@terminal3/t3n-sdk";

setEnvironment("testnet");

const T3N_API_KEY = process.env.T3N_API_KEY;
if (!T3N_API_KEY)
  throw new Error("Missing T3N_API_KEY");

const rl = createInterface({ input, output });

const email =
  await rl.question("Email for OTP: ");
const firstName =
  await rl.question("First name: ");
const lastName =
  await rl.question("Last name: ");
const dateOfBirth =
  await rl.question("DOB YYYY-MM-DD: ");
const gender =
  await rl.question("Gender m/f: ");

const wasmComponent = await loadWasmComponent();
const address = eth_get_address(T3N_API_KEY);

const client = new T3nClient({
  wasmComponent,
  handlers: {
    EthSign: metamask_sign(
      address, undefined, T3N_API_KEY),
  },
});

await client.handshake();
const did = await client.authenticate(
  createEthAuthInput(address));
console.log("Authenticated DID:", did.value);

await client.runOtpThenUserInput({
  channel: "email",
  emailAddress: email,
  profile: {
    first_name: firstName,
    last_name: lastName,
    date_of_birth: dateOfBirth,
    gender,
    email_address: email,
  },
  getOtpCode: async () =>
    await rl.question("Enter OTP code: "),
});

rl.close();
console.log("Profile updated.");
Live Walkthrough · Build → Register → Authorize → Profile → Invoke14 / 23
Step 6Invoke

Invoke & watch it run

  • Authenticate as the agent (AGENT_KEY or T3N_API_KEY)
  • search-offers — the tool calls Duffel, returns offers
  • Choose an offer confirm before calling book-offer with its ids
  • Get a PNR — a Duffel test order comes back
✅ An agent calling two contract tools, end to end
$ node scripts/invoke-flight-demo.mjs Agent DID: did:t3n:… search-offers LHR → JFK ↳ available offers table ? Select an offer to book: 1 ? Book this offer? y book-offer selected offer ↳ order: ord_… (test PNR) ✓
Live Walkthrough · Build → Register → Authorize → Profile → Invoke15 / 23
Step 6Script

invoke-flight-demo.mjs

agent calls search-offers → book-offer
import {
  T3nClient,
  setEnvironment,
  loadWasmComponent,
  eth_get_address,
  metamask_sign,
  createEthAuthInput,
  getNodeUrl,
  getScriptVersion,
} from "@terminal3/t3n-sdk";
import { stdin as input, stdout as output } from "node:process";
import { createInterface } from "node:readline/promises";

setEnvironment("testnet");

const AGENT_KEY = process.env.AGENT_KEY || process.env.T3N_API_KEY;
const TENANT_SCRIPT = process.env.TENANT_SCRIPT;

if (!AGENT_KEY) throw new Error("Missing AGENT_KEY");
if (!TENANT_SCRIPT) throw new Error("Missing TENANT_SCRIPT");

const wasmComponent = await loadWasmComponent();
const agentAddress = eth_get_address(AGENT_KEY);

const agentClient = new T3nClient({
  wasmComponent,
  handlers: {
    EthSign: metamask_sign(agentAddress, undefined, AGENT_KEY),
  },
});

await agentClient.handshake();
const did = await agentClient.authenticate(createEthAuthInput(agentAddress));
console.log("Agent DID:", did.value);

const scriptVersion = await getScriptVersion(getNodeUrl(), TENANT_SCRIPT);

console.log("Calling search-offers...");
const search = await agentClient.executeAndDecode({
  script_name: TENANT_SCRIPT,
  script_version: scriptVersion,
  function_name: "search-offers",
  input: {
    origin: "LHR",
    destination: "JFK",
    departure_date: "2026-07-15",
    cabin_class: "economy",
    adult_count: 1,
  },
});

if (!search.offers?.length)
  throw new Error("No offers returned");

console.log("\nAvailable offers:");
console.table(formatOfferRows(search.offers));

const rl = createInterface({ input, output });
let offer;
let shouldBook = false;

try {
  while (!offer) {
    const answer = await rl.question(
      `Select an offer to book (1-${search.offers.length}): `,
    );

    try {
      offer = search.offers[parseOfferSelection(answer, search.offers.length)];
    } catch (error) {
      console.log(error.message);
    }
  }

  console.log("\nSelected offer:");
  console.table([formatSelectedOffer(offer)]);

  const confirmation = await rl.question("Book this offer? (y/N): ");
  shouldBook = parseConfirmation(confirmation);
} finally {
  rl.close();
}

if (!shouldBook) {
  console.log("Booking cancelled.");
  process.exit(0);
}

console.log("Calling book-offer...");
const booking = await agentClient.executeAndDecode({
  script_name: TENANT_SCRIPT,
  script_version: scriptVersion,
  function_name: "book-offer",
  input: {
    offer_id: offer.id,
    passenger_id: offer.passenger_ids[0],
    total_amount: offer.total_amount,
    total_currency: offer.total_currency,
  },
});

console.dir(booking, { depth: null });

function formatOfferRows(offers) {
  return offers.map((offer, index) => ({
    "#": index + 1,
    offer_id: offer.id,
    amount: formatAmount(offer),
    expires_at: offer.expires_at || "n/a",
  }));
}

function formatSelectedOffer(offer) {
  return {
    offer_id: offer.id,
    passenger_id: offer.passenger_ids?.[0] || "n/a",
    amount: formatAmount(offer),
    expires_at: offer.expires_at || "n/a",
  };
}

function parseOfferSelection(input, offerCount) {
  const trimmed = input.trim();
  const selected = Number.parseInt(trimmed, 10);

  if (!/^\d+$/.test(trimmed) || selected < 1 || selected > offerCount) {
    throw new Error(`Choose a number from 1 to ${offerCount}`);
  }

  return selected - 1;
}

function parseConfirmation(input) {
  return ["y", "yes"].includes(input.trim().toLowerCase());
}

function formatAmount(offer) {
  return `${offer.total_amount} ${offer.total_currency}`;
}
Live Walkthrough · Build → Register → Authorize → Profile → Invoke16 / 23
Tie It Together

What you just learned

IDENTITY

Wallet → DID

Your agent signs in and gets a real, verifiable identity.

TOOLS

Outbound HTTP

It reaches the outside world — APIs, services, data.

MEMORY

KV store

It remembers across calls — preferences, state, history.

CONFIDENTIAL

TEE execution

All of it runs sealed — sensitive data never leaks.

Four primitives. Endless agents.

Terminal3 ADK Workshop17 / 23
Bounty Guidance

From demo to submission

Today's agent is already 80% of a bounty entry. Three moves turn it into a submission:

01 · PICK

Choose a use case

Swap "flights" for a problem you actually care about — same primitives, new domain.

02 · EXTEND

Add real value

One genuine tool + one piece of memory is enough to be useful. Ship that first.

03 · SHIP

Submit & demo

Register it, record a short run, write up what it does. Done.

→ You don't need scale. You need one agent that clearly works.

Terminal3 ADK Workshop18 / 23
Bounty GuidanceIdeas

Five ideas to steal

All buildable with one tool + memory. Pick one, make it yours.

TRAVEL

Booking concierge

Hotels, restaurants, or transport — search + remembered preferences.

SME OPS

Payroll agent

Run pay calculations over sealed employee data — the ADK's own use case.

PRODUCTIVITY

Data-fetch assistant

Pull live data (prices, weather, status) and act on saved rules.

STARTUP

Lead qualifier

Score inbound leads against criteria, remember every prospect.

AUTOMATION

Workflow trigger

Watch a condition, call an API when it's met, log what it did.

YOUR IDEA

__________

The best entry is the one you'd actually use. What's yours?

Terminal3 ADK Workshop19 / 23
Bounty GuidanceHow to Win

How to stand out

  • Make it run. A working demo beats a big idea on a slide.
  • Use the platform's edge. Lean into confidential data — that's T3's whole point.
  • Tell the story. What problem, who for, why it matters. 60 seconds.
  • Scope tight. One sharp feature, finished, wins over five half-built.
  • Team up. Collaboration is encouraged — find a partner tonight.
  • Ask early. The dev Telegram is there — use it before you're stuck.
📋 Submission details on the hackathon page — QR coming up
Terminal3 ADK Workshop20 / 23
?
Questions
What do you want to build? What's in your way? Let's talk.
Terminal3 ADK Workshop21 / 23
Keep Building

Everything you need

Scan, claim your tokens, and ship your bounty entry. Join the community for the next builder session.

Terminal3 × Kracked Devs22 / 23
Now go build.
Thank you for hacking with us tonight. Grab a refreshment, find a teammate, and tell us what you're making.
Terminal3 × Kracked Devs · Builder Meetup · 19 June 2026
Shi Wei · Workshop Instructor23 / 23
Edit mode — click text · Ctrl+S to save
01 / 23