---
name: t3n-adk-quickstart
description: Scaffold and debug a Terminal 3 (T3N) ADK integration — use when the user wants to "build with Terminal 3", "use the T3N SDK", or "add a TEE contract" to their app.
---
# Building with Terminal 3 ADK
You are helping a developer — possibly a first-timer, possibly vibe-coding — integrate the Terminal 3 Agent Developer Kit. Follow this order. Do not jump to contract code or advanced features before step 3 actually works. Do the scaffolding yourself — don't just print code and tell the user to run it.
## Step 1: Confirm the basics before writing any contract code
Ask the user (don't assume):
1. Do they already have a T3N API key and test credits? If not, point them to the claim page linked from `/developers/adk/get-started/prerequisites/request-test-tokens` on the Terminal 3 docs — it's self-serve, no approval wait.
2. Will an **AI agent** — not just their own app — be making calls on its own, separately from the code they're writing in this session? (Those calls run through a **contract**: a small program that does the actual work inside a secure enclave, covered in Step 4.) If so, the agent needs its **own** separate key and its **own** test credits — an agent's balance is separate from the developer's own and starts at zero. Get it a second key from the same claim page (revisiting it mints a fresh key+credits each time) rather than reusing the developer's own `T3N_API_KEY` — see `/developers/agents/register-agent`. Reusing the developer's own key or skipping this is the most common cause of `InsufficientCreditError` on agent calls. If the agent belongs to an **organization** rather than being self-registered, the organization can fund it directly from the org's balance, or an admin can fund it from **their own** balance with `fundAgent(agentDid, amount)` (still requires the admin to administer an org the agent belongs to) — see the funding section at `/developers/adk/support/ai-coding-assistants#self-service-agent-credit-funding`.
3. Is this a Next.js/Vite/Webpack project? If so, be ready for possible WASM-loading friction — see the troubleshooting note in `/developers/adk/get-started/quickstart`.
## Step 2: Scaffold the project BEFORE writing the connection code
A blank folder needs this before the SDK's `await`-based sample code will even parse:
\`\`\`bash
mkdir -p my-t3n-app && cd my-t3n-app
npm init -y
npm pkg set type=module # required — the sample code uses top-level await
npm install @terminal3/t3n-sdk tsx # no version pin — this SDK ships frequently, always install latest
\`\`\`
Then get the user's `T3N_API_KEY` (from the claim page) and export it into the shell you'll run scripts from — don't write it into a file:
\`\`\`bash
export T3N_API_KEY="<their key>"
\`\`\`
## Step 3: Get a basic authenticated connection working FIRST
Save this as `quickstart.ts` in the project you just scaffolded, and actually run it — don't just write it:
\`\`\`typescript
import {
T3nClient,
setEnvironment,
loadWasmComponent,
resolveTrustAnchor,
eth_get_address,
metamask_sign,
createEthAuthInput,
} from "@terminal3/t3n-sdk";
setEnvironment("testnet"); // the public SDK defaults to testnet — set it explicitly so your target cluster is unambiguous
const T3N_API_KEY = process.env.T3N_API_KEY!;
const wasmComponent = await loadWasmComponent();
const address = eth_get_address(T3N_API_KEY);
const t3n = new T3nClient({
trustAnchor: await resolveTrustAnchor("testnet"), // required — T3nClient throws without it
wasmComponent,
handlers: { EthSign: metamask_sign(address, undefined, T3N_API_KEY) },
});
await t3n.handshake();
const did = await t3n.authenticate(createEthAuthInput(address));
const tenantDid = did.value; // did:t3n:... — "tenant" is T3N's term for your authenticated identity; keep this variable, reuse it in every later step
console.log("Connected as:", tenantDid);
\`\`\`
Run with `npx tsx quickstart.ts`. Do not proceed to contract code until this prints a `did:t3n:...` value.
**Keep building in this same file.** `t3n` and `tenantDid` are plain JS variables scoped to this one running script. Later snippets (`TenantClient` construction, contract registration, invocation) all assume `t3n`/`tenantDid`/`tenant` are already in scope — append new code to the bottom of `quickstart.ts` rather than starting fresh files, unless you deliberately repeat the connect-and-authenticate block at the top of a new one.
## Step 4: Contract code lives in a SEPARATE project
The Rust/WASM contract is not part of the Node project above — it's its own crate. Clone the reference implementation instead of hand-typing it, as a sibling folder (not nested inside your Node project):
\`\`\`bash
cd ..
git clone https://github.com/Terminal-3/z-tenant-flight.git
cd z-tenant-flight
rustup target add wasm32-wasip2 # install rustup first via https://rustup.rs if `rustup`/`cargo` aren't found
cargo build --target wasm32-wasip2 --release
\`\`\`
This produces `target/wasm32-wasip2/release/z_tenant_flight.wasm`. Back in `my-t3n-app/quickstart.ts` for the registration step, point `WASM_PATH` across the two folders, e.g. `../z-tenant-flight/target/wasm32-wasip2/release/z_tenant_flight.wasm`.
## Step 5: Known pitfalls — check these BEFORE assuming it's your bug
| Symptom | Likely cause | Fix |
|---|---|---|
| `Top-level await is currently not supported with the "cjs" output format` | No `package.json`, or missing `"type": "module"` | `npm pkg set type=module` (see Step 2) |
| `Invalid Ethereum private key` from `eth_get_address` | `T3N_API_KEY` was never exported into the shell running the script | `export T3N_API_KEY="..."` before running (see Step 2) |
| `T3nClient: \`trustAnchor\` is required and must be either a TrustAnchor ... or the explicit opt-out ...` | `trustAnchor` was omitted from the `T3nClient` config — it's required, not optional, and the client throws immediately at construction | Pass `trustAnchor: await resolveTrustAnchor(env)` (see Step 3) |
| Wall of `command not found` / `syntax error near unexpected token` lines, nothing like a TypeScript error | The code block was pasted directly at the shell prompt instead of into a `cat > file << 'EOF'` heredoc — bash tried to run each line as a command | Always write the whole `cat > quickstart.ts << 'EOF' ... EOF` block as one unit, never just the code in between (see Step 3) |
| `-bash: !: event not found` | Bash history expansion on a bare `!` (TypeScript's non-null assertion, e.g. `T3N_API_KEY!`) — happens both at a raw prompt and inside an unquoted `<< EOF` heredoc | Quote the heredoc delimiter: `<< 'EOF'`, not `<< EOF` |
| `ReferenceError: tenant is not defined` / `tenantDid is not defined` in a later snippet | Each doc page's code sample assumes earlier variables are still in scope — they aren't, across separate files | Append new code to the same running file instead of starting a new one (see Step 3) |
| `ENOENT` reading a `.wasm` file | `WASM_PATH` is relative to the wrong project — the contract lives in its own cloned repo, not the Node project | Point `WASM_PATH` across folders, e.g. `../z-tenant-flight/target/...` (see Step 4) |
| WASM/module parse error, works with `tsx` but not `node`/webpack | Bundler compatibility | Try a plain Node script or server-only route first to isolate the bundler from the SDK |
| Generic `HTTP 500` with no detail | Could be egress not authorized, wrong action name, or a real platform issue — not necessarily your code | Check the outbound-HTTP-grant requirement below before assuming it's a bug; if it's consistent and reproducible, it may be platform-side |
| Contract can't reach an external API even though the code looks right | Outbound HTTP is authorized by the **calling user's grant**, not the contract itself | Confirm a user (or self-grant, for direct calls) actually authorized that host via `member-delegation-update` |
| `tenant not found` | The DID was derived/hardcoded instead of read from the session | Always read `tenantDid`/`did.value` from the authenticated session, never construct it |
| `AccessDenied` reading your own contract's secret | KV map ACLs default to deny | Explicitly set `readers`/`writers` when creating the map |
| Map path looks right but reads/writes silently miss | `tenant_did()` returns raw bytes and must be hex-encoded once when building the `z:<tid>:` map path | Build the path with `hex::encode(&tenant_did())` — a missing OR double hex-encode both produce a path that matches nothing |
| Re-registering a tail allocates a new `contract_id` | Explicit pinned versions in `contracts.execute()` are still honored; the real gotcha is map ACLs scoped to the old `contract_id` going stale (there's no API to fetch the current one) | Keep a record of each `contract_id`; pass an explicit version to pin, and re-grant map access after re-registering if needed |
| Agent can authenticate but its calls fail with `host/http.egress_denied` | The agent is authenticated but no one has authorized it yet — those are two different steps | The **user (data owner)**, not the agent, must sign a `member-delegation-update` grant naming the agent's DID before it can do anything with outbound HTTP |
## Step 6: Where to look things up
- Full onboarding walkthrough, starting point: `/developers/adk/get-started/quickstart`
- Every confirmed SDK method, host interface, and REST tag: `/developers/adk/reference`
- Common errors reference: `/developers/adk/tips/common-errors`
- Conceptual map of the whole ADK: `/developers/adk/overview/adk-tour`
- Funding an agent's credit balance: `/developers/adk/support/ai-coding-assistants#self-service-agent-credit-funding` below
## Step 7: When something seems broken and isn't in this list
Say so plainly to the user rather than guessing. Some rough edges are real platform issues with no clean workaround, not something you can code around. Don't fabricate a fix — point them at the developer Telegram (`https://t.me/terminal3developer`) or `devrel@terminal3.io`.
---
*Compatibility: this file uses Claude Code's Skill frontmatter (`name`/`description`). For Codex, save everything below the frontmatter as `AGENTS.md` in your project root — Codex reads it automatically. For Cursor, save everything below the frontmatter as `.cursorrules`. For Windsurf or any other tool that accepts a plain system-context file, paste the body as-is.*