Terminal3
×
Building
AI Agents
What's an AI agent?
Not just a chatbot. An agent perceives input, decides what to do, and acts — powered by tools and memory.
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.
Confidential by design
Agents execute inside a Trusted Execution Environment (TEE) — data stays sealed, even from the host.
Wallet-native identity
Each agent has a real identity (a DID), authenticated by your wallet — no scattered API keys.
An SDK, not a maze
The ADK gives you the primitives — tools, data, deployment — in a single TypeScript + Rust workflow.
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.
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.
→ Build a real agent on the ADK, submit it, and you're in the running. We'll cover exactly how near the end.
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.
search-offers
Asks Duffel for flight offers. Needs only the Duffel test token — no passenger data at all.
book-offer
Books one test offer. Passenger PII is filled by the host from your profile via {{placeholders}} — never inside the agent.
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.
T3N API key
Your login to the Terminal3 network (T3N).
Your DID
Returned when you authenticate — your identity and namespace owner.
Tenant contract
The Rust → WASM flight contract, registered under your namespace.
Agent call
An authenticated session calls the contract's functions.
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
Note · for a first demo, the same T3N key can act as user, tenant admin, and agent.
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.
Publish the WASM
Registered under z:<did>:travel-contracts — now agents can call it.
Private KV store
Create z:<did>:secrets — readable & writable only by the contract.
Stash the token
Write duffel_api_key. The contract reads it at runtime — the agent never sees it.
setup-flight-demo.mjs
register · create secrets map · seed Duffel tokenimport { 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,
});
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.
search-offers · book-offer
Whitelist exactly the contract functions the agent may call.
allowedHosts
Permit api.duffel.com — the only host this agent can reach.
No grant, no call
Skip this and Duffel calls fail with an egress / permission error.
authorize-agent.mjs
self-grant search-offers · book-offer · api.duffel.comimport {
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);
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.
- Verify your email — an OTP is sent and confirmed
- Store profile fields name, DOB, gender, email
- Plaintext PII never enters the contract's WASM
update-flight-profile.mjs
verify email via OTP · store profile fieldsimport { 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.");
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
invoke-flight-demo.mjs
agent calls search-offers → book-offerimport {
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}`;
}
What you just learned
Wallet → DID
Your agent signs in and gets a real, verifiable identity.
Outbound HTTP
It reaches the outside world — APIs, services, data.
KV store
It remembers across calls — preferences, state, history.
TEE execution
All of it runs sealed — sensitive data never leaks.
Four primitives. Endless agents.
From demo to submission
Today's agent is already 80% of a bounty entry. Three moves turn it into a submission:
Choose a use case
Swap "flights" for a problem you actually care about — same primitives, new domain.
Add real value
One genuine tool + one piece of memory is enough to be useful. Ship that first.
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.
Five ideas to steal
All buildable with one tool + memory. Pick one, make it yours.
Booking concierge
Hotels, restaurants, or transport — search + remembered preferences.
Payroll agent
Run pay calculations over sealed employee data — the ADK's own use case.
Data-fetch assistant
Pull live data (prices, weather, status) and act on saved rules.
Lead qualifier
Score inbound leads against criteria, remember every prospect.
Workflow trigger
Watch a condition, call an API when it's met, log what it did.
__________
The best entry is the one you'd actually use. What's yours?
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.
Everything you need
Scan, claim your tokens, and ship your bounty entry. Join the community for the next builder session.

