# Changelog Source: https://docs.terminal3.io/developers/adk/changelog What's changed in these docs, and what we know about SDK version history so far. This page currently tracks **documentation** changes we can verify directly, since we don't yet have a confirmed, complete SDK release history to publish. If you need details on a specific past SDK release, ask in the [developer Telegram](https://t.me/terminal3developer) — we'll backfill verified entries here as we confirm them. ## Documentation ### 2026-09-21 * Confirmed against the `testnet-v1.0.11` release (current SDK `5.19.0`): updated [Member Delegation](/developers/adk/get-started/member-delegation) for the breaking grant-shape change below — the "What a grant can say" table and both code samples now show one function per grant row. ### 2026-09-18 * Confirmed against the `testnet-v1.0.10` release (current SDK `5.16.0`): documented the full agent lifecycle — `createAgent` (now with an `initialCredits` option), `fundAgent`, `deleteAgent`, `updateAgent`, `deregisterAgent` — in the SDK section below, [Register an Organization-owned Agent](/developers/agents/provision-org-agent), and [Register a Public Agent](/developers/agents/register-agent). * Added a copy-paste AI-coding-assistant prompt pair (Claude-flavored and OpenAI-flavored) for self-service agent credit funding — see [Using AI Coding Assistants](/developers/adk/support/ai-coding-assistants). * Added `resolveTrustAnchor()` and the new required `rtmr1_allowlist` field to [Verify the trust anchor](/developers/adk/tips/verify-trust-anchor) — RTMR1, not RTMR3, is now the meaningful rootfs-integrity measurement. * Documented the organisation usage-report / call-count methods (`getOrgUsageReport`, `getOrgWorkflowExecutionsForWindow`, `getOrgActivityCountForWindow`), `getDelegation`'s new `grantee_kinds`, `countAgentsByContract`/`countAllAgentsByContract`, and `setDefaultQuotas`/`updateQuotas` in the [SDK & API Reference](/developers/adk/reference). * Marked the `ap2` (`ap2/mandate`) host interface **removed** in [Host API](/t3n/how-t3n-works/host-api) and the reference page — the interface and its linker world were deleted from the runtime in this release. * Updated [Common Errors](/developers/adk/tips/common-errors) for the new JSON error envelope on all 4xx/5xx responses (not just contract-level `bad_request`s), the rewritten egress-denied message, and the `agent-credit-transfer` error table. ### 2026-09-08 * Renamed "Agent Auth" to [Member Delegation](/developers/adk/get-started/member-delegation) throughout — the underlying vocabulary shift (`agent_auth`→`member_delegation`, `script`→`contract`) is covered in the SDK section below — and moved the page from `overview/` to `get-started/` to match its nav group and URL slug. * Documented the audit-log CSV export methods (see the SDK section below) and added the [Read your organization's activity log](/developers/adk/tips/get-activity-log) and [Running the code samples in this guide](/developers/adk/tips/running-code-samples) tip pages, plus [Verify the trust anchor](/developers/adk/tips/verify-trust-anchor) and a stateless `invoke()` guide on the Member Delegation page. * [SDK & API Reference](/developers/adk/reference): added `setEnvironment`'s `sandbox` value, split `tenant.contracts.execute()` from `client.executeAndDecode()` (they're on different objects), filled in `tenant.maps`' `delete`/`entrySet`/`entryGet`/`getStatus` methods, and corrected the `outbox`/`ap2` WIT host interfaces from "coming soon" to "Available" (both are live, CI-tested capabilities). * Fixed several stale references caught while re-verifying every page against the confirmed `testnet-v1.0.9` release: a duplicated code sample in [Quickstart](/developers/adk/get-started/quickstart), outdated host-interface version numbers in [Write your first TEE contract](/developers/adk/get-started/walkthrough/write-contract), and a broken link left over from the Member Delegation page move. ### 2026-07-06 * Added [Quickstart](/developers/adk/get-started/quickstart) — a single page to get an authenticated call working before touching contract code. * Filled in the previously-empty [ADK Tour](/developers/adk/overview/adk-tour) and [Member Delegation](/developers/adk/get-started/member-delegation) pages. * Added this [Changelog](/developers/adk/changelog) and the [SDK & API Reference](/developers/adk/reference) page. * Fixed a duplicate "Step 4" between the Invoke and Test walkthrough pages. * Expanded [Common Errors](/developers/adk/tips/common-errors) with generic-HTTP-500 triage guidance and a table of common integration gotchas (tenant DID double-encoding, `baseUrl` handling). * Added a warning about contract version-shadowing on re-registration to [Register your TEE contract](/developers/adk/get-started/walkthrough/register-contract). ## SDK ### Delegation grants: one function per grant, unified `scopes` (breaking) — confirmed live on testnet (`testnet-v1.0.11`, 2026-09-18) A `member-delegation-update` / `OrgDataClient.setDelegation` grant's shape changed, and the node's decoder rejects the old shape outright rather than accepting it silently: * **`functions: string[]` → `function: string`.** A grant now confers exactly one function. Authorising several functions on the same contract means several grant rows — the SDK's new `fanOutGrant(base, functions)` helper builds that array for you from a shared base. The contract keeps at most one grant per `(grantee, contract_id, function)`; a later write for the same triple replaces the earlier row. * **`scopes: string[]` + `read_scopes: string[]` → one `scopes: { path, access }[]` list.** `access` is a list of verbs (`"read" | "write" | "delete"`) — only `"read"` is enforced today. Build a read-only list with the new `readOnlyScopes(paths)` helper. A grant written under the old shape may still echo its `read_scopes` value as `legacy_read_scopes` on a read; new writes never set it. * **`updateMemberDelegation`'s read-merge-write key is now `(grantee, contract_id, function)`**, not `(grantee, contract_id)` — keying on the pair alone would let granting a second function on an already-granted contract silently drop the first function's row. * The **org delegation edge** (`OrgDataClient.setDelegation`) moved to the same `function` + `scopes` shape; it still carries no `version_req`/`allowed_hosts`/`window`. **SDK version at this release: `5.19.0`** (not the `5.17.0` some intermediate PR descriptions cite — two more minor bumps landed between that PR and the tagged release; always cite the tag's own `package.json`). See [Member Delegation](/developers/adk/get-started/member-delegation) for the updated grant examples. `client/mcp/t3n-mcp`'s `getDelegationInstructions` still advises the retired shape as of this release — a known gap on the MCP side, not fixed here. If you're driving delegation through the MCP sidecar rather than the SDK directly, use the shape on this page, not what that tool currently prints. ### Agent lifecycle: create with initial credits, fund, update, delete, deregister — confirmed live on testnet (`testnet-v1.0.10`, 2026-09-15) The agent lifecycle is now complete end to end. All of the below dispatch through `tee:organisation/contracts` or `tee:agent-registry/contracts` and are gated by contract-version preflights that throw (not silently no-op) against an older node. * **`createAgent(orgDid, name, options?)`** mints an org-owned agent and, since `5.16.0`, accepts `options.initialCredits` to pre-fund it **in the same transaction**, from the organisation's own T3N balance. A freshly created agent otherwise starts with zero credit and its first metered call fails. Needs `tee:organisation/contracts` ≥ 0.7.0 for `initialCredits`; the call throws rather than creating an unfunded agent silently against an older contract. * **`fundAgent(agentDid, amount, options?)`** (new in `5.16.0`) tops up an agent's balance any time after creation — from the caller's own balance by default, or from `options.fromOrg`'s balance (requires you to admin `fromOrg`, and the agent must actually be a member of it). Returns `{ seqNo }`, the ledger transaction's sequence number. See [Common Errors](/developers/adk/tips/common-errors#agent-credit-funding-createagent-s-initialcredits-fundagent) for the full authorisation-refusal table — refusals are always identity-based first, balance-based second. * **`deleteAgent(orgDid, agentDid)`** (`5.8.0`) tears down an org-owned agent — sweeps every org-data row it holds and deletes its registry record in one transaction. Its API keys stop authenticating immediately: the delete removes the key's *principal* (its DID-classification row), not each key individually. Needs `tee:organisation/contracts` ≥ 0.20.0. * **`updateAgent(orgDid, agentDid, { agentUri?, ownerDid? })`** (`5.8.0`) is an admin-side edit of an agent's registry record; at least one of `agentUri` / `ownerDid` is required. Needs `tee:organisation/contracts` ≥ 0.20.0. * **`deregisterAgent(agentDid?)`** (`5.8.0`) is how an agent removes its own registry footprint (registry record + any hosted cards). Omit `agentDid` to act on the caller's own session. Needs `tee:agent-registry/contracts` ≥ 1.7.0. CLI equivalents ship in the same `t3n` binary: `t3n agent create --org --name [--initial-credits ]`, `t3n agent fund --agent --amount [--note ] [--from-org ]`, `t3n agent delete --org `, `t3n agent update --org [--uri ] [--owner ]`, `t3n agent deregister []`. **Known gap:** there is currently no self-service way to fund an *organisation's own* balance — only a cluster operator can. A brand-new org starts at zero, so `--initial-credits` / `{ initialCredits }` on a fresh org fails with an insufficient-credit error; that's expected, not a bug. ### Trust anchor: `resolveTrustAnchor()`, the RTMR1 measurement, and SEV-SNP support — confirmed live on testnet (`testnet-v1.0.10`, 2026-09-15) * **`resolveTrustAnchor(env, opts?)`** is a new one-call convenience over `fetchTrustedManifest` — same verification, less boilerplate. Recommended default for constructing `T3nClient`'s `trustAnchor`. * **`TrustAnchor` gained a required `rtmr1_allowlist` field.** The node now measures rootfs integrity via dm-verity + a Unified Kernel Image, landing in RTMR1 — this is the real signal going forward. `rtmr3_allowlist` is kept for backward compatibility only (the boot script no longer extends RTMR3). See [Verify the trust anchor](/developers/adk/tips/verify-trust-anchor). * **Attestation is now vendor-aware: TDX and AMD SEV-SNP.** New optional trust-manifest fields `sev_snp_measurement_allowlist` / `sev_snp_product`, and new SDK exports `verifyDkgAttestation` (vendor-dispatching), `verifySevSnpReport`, `detectQuoteVendor`, `sevSnpVcekUrl`, `fetchSevSnpCollateral` — additive, alongside the existing TDX-only `verifyTdxQuote`. `resolveTrustAnchor`/`fetchTrustedManifest` handle either vendor transparently; most integrations against the shared testnet/production clusters won't need the lower-level exports directly. Ships as part of the same release's AWS bare-metal SEV-SNP deployment support (trinity#2183). ### Organisation usage & delegation — confirmed live on testnet (`testnet-v1.0.10`, 2026-09-15) * **`getOrgUsageReport(input)`**, **`getOrgWorkflowExecutionsForWindow(orgDid, fromMs, toMs)`**, and **`getOrgActivityCountForWindow(orgDid, fromMs, toMs)`** (via `createUsageReportClientFromSession`) replace an earlier, since-superseded set of call-count helpers shipped and renamed within this same release cycle (`getOrgCallCount*`, `getOrgAgentCallCountForWindow` — neither exists at `testnet-v1.0.10`; don't reference them). `getOrgWorkflowExecutionsForWindow` counts protected-workflow executions (`z:`/`y:` client-contract calls, any outcome); `getOrgActivityCountForWindow` counts every call, any contract, any outcome. Both require org-admin and both throw (rather than silently truncating) when a window is wider than the node scans in one call. * **`getDelegation()`** replies now include `grantee_kinds` (`"agent" | "human" | "org"` per grantee). New **`countAgentsByContract(input)`** / **`countAllAgentsByContract(input)`** count delegated agents per contract for an org, single-page or drained to exhaustion. Needs org-data ≥ 2.11.0. * **`tenant.setDefaultQuotas(opts)`** / **`tenant.updateQuotas(opts)`** set an organisation's default per-agent quotas and override them for a specific agent, respectively. ### AP2 host interface removed — confirmed on testnet (`testnet-v1.0.10`, 2026-09-15) The `ap2` (`ap2/mandate`) WIT host interface — previously listed as "Available" on this site — was removed from the runtime entirely: the interface, its linker world, and the backing KV policy row are gone. A contract that still imports `ap2/mandate` will fail to instantiate. No replacement has shipped as of this release. See [Host API](/t3n/how-t3n-works/host-api). ### Error responses: JSON envelope everywhere, clearer egress denials — confirmed live on testnet (`testnet-v1.0.10`, 2026-09-15) Framework-level rejections (malformed query strings, undecodable path segments, oversized request bodies) that used to come back as raw `text/plain` are now re-encoded into the standard `{ error, code, request_id }` JSON envelope, under the original HTTP status. Separately, the egress-denied error for a `member_delegation` scope mismatch now names **both** the rejected host(s) and the actual resolved allowlist, so a scheme/port mismatch is visible directly in the message instead of requiring a guess. See [Common Errors](/developers/adk/tips/common-errors). ### Delegation vocabulary — already live; the old names are deprecated, not yet removed Everything that used to be called a *grant* or *agent auth* on the request surface is now a *delegation*. The two edges are `member_delegation` (a member delegating their own authority) and `org_delegation` (an organisation granting authority over its contracts). **Client methods** | Deprecated (still works today) | Current | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- | | `getAgentAuth` | `getMemberDelegation` | | `updateAgentAuth` | `updateMemberDelegation` — the leading `agentDid` argument is gone; the `grantee` now lives on the grant itself | | `agentAuthUpdate` | `memberDelegationUpdate` | | `revokeAgentAuthForOrg` | `revokeMemberDelegationForOrg` | | `OrgDataClient.grantsGet` | `getDelegation` | | `OrgDataClient.setGrants` | `setDelegation` | | `OrgDataClient.deleteGrants` | `deleteDelegation` | | `OrgDataClient.removeMemberGrants` | `removeMemberDelegation` | **Types** | Deprecated (still works today) | Current | | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `UserGrant` | `BoundGrant` | | `OrgContractGrants` / `isOrgContractGrants` | `OrgContractDelegation` / `isOrgContractDelegation` | | `SetGrantsInput` / `DeleteGrantsInput` / `GrantsGetInput` | `OrgDelegationSetInput` / `OrgDelegationDeleteInput` / `OrgDelegationGetInput` | | `RemoveMemberGrantsInput` | `OrgRemoveMemberDelegationInput` | | `AgentAuthPolicy` and the `delegation/agent-auth` module | `MemberDelegationDoc`, `parseMemberDelegationDoc`, `mergeGrants`, `mergeDiscoverDids` from `delegation/member-delegation` | **Raw `execute` callers** — if you build the wire payload yourself rather than going through a client method, the function names and the body both changed: | Old | New | | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `agent-auth-update` / `agent-auth-get` / `agent-auth-revoke-org` | `member-delegation-update` / `member-delegation-get` / `member-delegation-revoke-org` | | `org-policy-set-grants` / `org-policy-delete-grants` / `org-grants-get` / `org-remove-member-grants` | `org-delegation-set` / `org-delegation-delete` / `org-delegation-get` / `org-remove-member-delegation` | | nested `agents[].scripts[]` | flat `grants[]` | | `agent_did` / `user_did` | `grantee` | | `script_name` | `contract_id` | | flat `valid_from_secs` / `valid_until_secs` | `window { … }` | camelCase keys (`agentDid`, `scriptName`, `validFromSecs`) are rejected outright rather than silently dropped, so a mistyped time-box can no longer be lost on the wire. **Verdicts** — `checkDelegation()` returns the node's verdict as received. Grant kinds are `member_delegation` / `org_delegation` (was `agent_auth` / `user_grant`) and the deny codes are `member_delegation_not_found` / `_expired` / `_not_yet_valid` (was `agent_auth_*`). Hackathon integrations have referenced `@terminal3/t3n-sdk` versions `3.5.2`, `3.9.0`, and `3.11.0` in the wild. We haven't cross-checked these against an official release history yet, so we're not listing per-version changes here until we can confirm them — a placeholder is worse than an honest gap. ### Audit log CSV export — confirmed live on testnet (`testnet-v1.0.9`, 2026-08-27) Three new `T3nClient` methods export your organisation's activity audit log to CSV, built on the existing `getActivityLog` read: * **`client.startActivityLogExport(opts?)`** takes the same scope/filters as `getActivityLog` and starts a background export, returning an `export_id`. Only one export runs at a time per caller — starting a second while one is in flight is refused. * **`client.getActivityLogExportStatus(exportId)`** reports `pending` / `ready` / `failed`. Only your own export ids resolve — someone else's reads as absent, not as a permissions error. * **`client.downloadActivityLogExport(exportId)`** fetches the finished CSV in one response. Stays fetchable until your next export replaces it. * **`client.exportActivityLog(opts?, poll?)`** is a convenience wrapper: starts the export, polls until ready (default 1s interval, 5 minute timeout, both configurable via `poll`), and returns the file. Use the three calls above directly when a UI needs to own its own polling — e.g. to survive a page reload. The exported CSV's columns are a fixed wire format: `seq_no`, `timestamp_ms`, `caller_type`, `actor`, `on_behalf_of`, `org`, `contract`, `function`, `outcome`. Notably absent: `roles` (doesn't fit a fixed column) and the per-row `hash` from the JSON read. `getAuditEvents()` and `getActivityLog()` themselves predate this export feature and were already real, public `T3nClient` methods — they just hadn't been written up here yet. Both are now in the [SDK & API Reference](/developers/adk/reference) table alongside the export methods. # Member Delegation Source: https://docs.terminal3.io/developers/adk/get-started/member-delegation How a member delegates scoped authority to an AI agent (or another principal), and how an agent authenticates to T3N. This page covers two separate things that are easy to conflate: an agent **authenticating** to T3N (proving who it is), and an agent being **authorized** to call a specific contract function (proving it's allowed to). If you know OAuth, delegation is the closest equivalent — a scoped, revocable grant a user hands out — except the grantee is an agent acting on its own, not a third-party app redirecting a user through a consent screen. This is about **agents** acting through your contract. If you're looking for how *you*, the tenant developer, authenticate to manage your own deployment, see [Set Up Development Environment](/developers/adk/get-started/prerequisites/set-up-dev-env) instead — that's a different session. This page assumes the agent **already has a DID**. Creating that identity in the first place — minting the agent and publishing its agent card — is [Agent Onboarding](/developers/agents/register-agent), which happens before anything on this page. That identity also needs its **own** test credits before any metered call on this page will work — an agent DID's balance is separate from its tenant's and starts at zero. Get it a key from the same [claim page](/developers/adk/get-started/prerequisites/request-test-tokens) you used for your own; it comes with credits attached. ## 1. An agent authenticates like any other T3N session An agent has its own identity — its own key pair and its own DID — separate from the tenant that owns the contract it's calling. It authenticates the same way any T3N client does: handshake, then authenticate, then read its own DID back from the session. There's nothing tenant-specific to configure here. ```typescript theme={null} import { T3nClient, loadWasmComponent, createEthAuthInput, eth_get_address, metamask_sign, fetchTrustedManifest, } from "@terminal3/t3n-sdk"; const agentKey = process.env.AGENT_KEY!; // a separate key for the agent — never reuse your tenant's T3N_API_KEY const agentAddress = eth_get_address(agentKey); const agentClient = new T3nClient({ trustAnchor: await fetchTrustedManifest("testnet"), wasmComponent, handlers: { EthSign: metamask_sign(agentAddress, undefined, agentKey), }, }); await agentClient.handshake(); const agentDid = await agentClient.authenticate(createEthAuthInput(agentAddress)); ``` Never hard-code or derive an agent's DID any more than you would a tenant's — always read it back from the authenticated session, exactly as `agentDid` is read above. And generate `AGENT_KEY` as its own separate credential (the same way you'd generate any Ethereum-style keypair) — don't reuse your tenant's `T3N_API_KEY` for an agent. ## 2. Being authenticated is not being authorized Authenticating proves the agent's identity. It does **not** grant it permission to call anything. Before an agent can invoke a contract function — especially one that makes an outbound HTTP call — the **user who owns the data** (the "data owner," not the agent, and not you as the tenant developer) has to explicitly grant that agent access: ```typescript theme={null} // Signed by the user (data owner), not the agent. await userClient.execute({ contract_id: "tee:user/contracts", contract_version: userContractVersion, function_name: "member-delegation-update", input: { grants: [ { grantee: agentDid, // the agent being authorized contract_id: TENANT_CONTRACT, // z::your-contract-tail version_req: contractVersion, function: "search-offers", // exactly one function per grant row scopes: [{ path: "search-offers", access: ["read"] }], allowed_hosts: ["api.duffel.com"], // exactly which external hosts }, { grantee: agentDid, contract_id: TENANT_CONTRACT, version_req: contractVersion, function: "book-offer", // a second function → a second row scopes: [{ path: "book-offer", access: ["read"] }], allowed_hosts: ["api.duffel.com"], }, ], }, }); ``` `member-delegation-update` **replaces the member's entire delegation document.** Whatever you pass as `grants` becomes the complete new policy, so a write like the one above drops every *other* agent's grants, and `grants: []` revokes all delegated access. To change one grant while leaving the rest intact, read the current policy first (`member-delegation-get`) and write back the merged list — or let the SDK do it for you. The same applies to `discover_dids`: it is a document-level field, so a write that omits it clears the member's whole discovery list. Read-merge-write, or omit it deliberately. To add or change grants safely, use the SDK's `updateMemberDelegation`. It reads the current policy, merges your grant(s) by `(grantee, contract_id, function)`, and writes the whole document back, so every other grant survives. It accepts a single grant or an array — pass one row per function, or build the array with the SDK's `fanOutGrant` helper when several functions share everything else: ```typescript theme={null} // `userClient` is the data owner's authenticated T3nClient (see below). import { fanOutGrant, readOnlyScopes } from "@terminal3/t3n-sdk"; await userClient.updateMemberDelegation( fanOutGrant( { grantee: agentDid, contract_id: TENANT_CONTRACT, version_req: contractVersion, scopes: readOnlyScopes(["search-offers", "book-offer"]), allowed_hosts: ["api.duffel.com"], }, ["search-offers", "book-offer"], ), ); ``` **Why "member delegation".** The name is for the **delegator**: a member (the data owner) delegates a scoped slice of their own authority to a *grantee*. The grantee is a member too — identified by a DID, whether that's an AI agent (as above) or the member's own DID for a direct call (a self-grant). The same `member-delegation-update` / `member-delegation-get` contract functions (SDK: `updateMemberDelegation` / `getMemberDelegation`) cover both. This page was previously titled "Agent Auth". `userClient` here is the data owner's own authenticated session — built the same way as `t3n`/`agentClient` above, just with the user's own key. See [Invoke your contract](/developers/adk/get-started/walkthrough/invoke-contract) for the full construction and where `TENANT_CONTRACT`/`contractVersion` come from. A grant is scoped three ways at once: which contract, which function on it, and which external hosts it may reach. An agent with no matching grant can still call the contract — the call just fails at the point it tries to reach the network, with `host/http.egress_denied`. ### What a grant can say Every field is snake\_case on the wire, exactly as written here — camelCase keys (`agentDid`, `scriptName`, `validFromSecs`) are rejected rather than silently dropped, so a mistyped time-box can't be lost. **Grant shape changed in `testnet-v1.0.11` (`@terminal3/t3n-sdk` 5.19.0+).** A grant now confers exactly **one** function — authorising several functions on the same contract means several grant rows, each carrying its own `scopes` (fan out with the SDK's `fanOutGrant` helper, shown above). The contract keeps at most one grant per `(grantee, contract_id, function)`; a later row with the same triple replaces the earlier one. This replaces the earlier shape, where one grant held a `functions` array plus a parallel `read_scopes` cap. The old shape is now rejected outright (the node's decoder is strict about unknown/missing fields) — a client still on the old shape needs `@terminal3/t3n-sdk` 5.19.0 or newer. | Field | Required | Meaning | | ----------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `grantee` | yes | The principal this grant applies to — an agent's DID, or the member's own for a self-grant. | | `contract_id` | yes | The target contract. `"*"` is accepted but authorises only the core-allowlist contracts, never your own tenant contract — name it exactly. | | `function` | yes | The single WIT function this grant confers (e.g. `"search-offers"`), or `"*"` for every function on that contract. One function per grant row — see the note above. | | `scopes` | yes | Data scope paths the grantee may access via `function`, as a list of `{ path, access }` entries. `access` is a list of verbs (`"read"` \| `"write"` \| `"delete"`) — **only `"read"` is currently enforced**; don't treat granting or withholding `"write"`/`"delete"` as a security boundary yet. Build a read-only list with the SDK's `readOnlyScopes(paths)`. | | `version_req` | no | A semver requirement on the target contract version. Omitted matches any version. | | ~~`read_scopes`~~ | *(retired)* | The old per-grant cap-subset mechanism, removed in `testnet-v1.0.11`. Since a grant now covers one function already, express a function's narrower read access as that function's own `scopes` instead. A grant written before this change may still echo its old value as `legacy_read_scopes` on a read — informational only, and new writes never set it. | | `allowed_hosts` | no | The egress hosts this grant lets the contract reach. Omitted means none — which is what produces `host/http.egress_denied`. | | `window` | no | A validity time-box, `{ valid_from_secs, valid_until_secs }`. Both bounds are optional unix-seconds; an omitted upper bound is capped by the operator, not left open. | ### Org delegation — the second edge Everything above is the **member** edge: a data owner delegating their own authority. There is a second, independent edge — **org delegation** — where an organisation admin grants authority over the org's contracts, written with the SDK's `OrgDataClient.setDelegation` rather than `updateMemberDelegation`. A call on the delegated path needs **both** edges to permit it. They are enforced separately rather than as one combined check, so a missing org grant and a missing member grant fail in different places. An org grant also carries less: only `function` and `scopes` — the qualifiers `version_req`, `allowed_hosts` and `window` are rejected on that edge, so an org grant cannot carry an expiry or a host allowlist. `checkDelegation()` reports which edge is missing for a given action. `getDelegation()`'s reply additionally carries `grantee_kinds` — one `"agent" | "human" | "org"` per grantee, so a single read partitions a contract's grantees without a separate roster call. Splitting authentication from authorization means a compromised or misbehaving agent key doesn't automatically mean compromised data access — the blast radius of a leaked agent key is exactly whatever contracts, functions, and hosts a user has explicitly granted it, nothing more. It also means a user can revoke an agent's access without the agent's key changing at all — they just stop re-issuing the grant. ## 3. Stateless invocation with invoke() Everything above assumes a session: `handshake()` then `authenticate()`, held open across calls. An org-owned agent has a second, stateless option — `invoke()` posts once to `POST /api/invoke` and authenticates by an opaque API key instead, with **no handshake, no `Session-Id`, no session state at all**: ```typescript theme={null} import { invoke, InvokeError } from "@terminal3/t3n-sdk"; try { const result = await invoke({ baseUrl: "https://cn-api.sg.testnet.t3n.terminal3.io", apiKey: process.env.AGENT_API_KEY!, // t3n_key_<...> — see Provision an org-owned agent request: { contract_id: TENANT_CONTRACT, contract_version: contractVersion, function_name: "search-offers", input: { origin: "LHR", destination: "JFK", departure_date: "2026-07-15", cabin_class: "economy", adult_count: 1 }, }, }); } catch (err) { if (err instanceof InvokeError) { // A fixed, generic message — never the raw api key, response body, or // underlying network error. Safe to log as-is. console.error(err.message, err.status); } throw err; } ``` The agent presents its key verbatim in the `X-T3N-Api-Key` header (`t3n_key_.`) — the same key printed once when you minted the agent, see [Register an Organization-owned Agent](/developers/agents/provision-org-agent). The node derives the acting agent DID from the key per call and runs the same execution pipeline the session `execute` path runs, returning the contract's decoded result directly (no JSON-RPC envelope to unwrap). `invoke()` refuses to send the api key over anything but HTTPS, unless the host is a local-dev loopback (`localhost` / `127.0.0.1` / `[::1]`) — an insecure `baseUrl` throws before any network activity. This is a fixed transport rule, not something you configure. Use `invoke()` for a one-shot call from somewhere a long-lived session doesn't fit — a webhook handler, a serverless function, a cron job — and the session-based flow above (`T3nClient.handshake()`/`authenticate()`) for anything that makes several calls and can hold a session open. `pii_did` on the request is the delegated-call target — set it to act on behalf of another org member; omit it for a self call. ## Direct (self) calls work the same way If a user is invoking their own contract directly rather than through a separate agent, the same grant mechanism applies — they just grant to their own DID (a self-grant) instead of an agent's. ## Full working example See [Invoke your contract](/developers/adk/get-started/walkthrough/invoke-contract) for this in context, including the search/book contract call itself. For why the outbound call fails without a grant even when the contract code is correct, see [Outbound HTTP calls are authorized by the user, not the contract](/developers/adk/tips/outbound-http-auth-by-user). Older community code sometimes references a standalone *delegation-credential* API — functions for building and signing a per-call delegation credential (an "envelope"). That flow was removed when T3N moved to **envelope-free** authorisation, and it is not part of the current SDK (confirmed against `testnet-v1.0.11`, `@terminal3/t3n-sdk` 5.19.0). Authority is now the standing on-chain member delegation shown above (intersected with any org-granted authority), so `member-delegation-update` — not a per-call credential — is the write surface. # Claim Your API Key & credits Source: https://docs.terminal3.io/developers/adk/get-started/prerequisites/request-test-tokens Claim your account, key, and test credits in one step — self-serve, no approval. Eager to start building on our ADK? You will need test credits to use in your contracts and for interactions with the T3 Network. **No approval, no waitlist.** This is fully self-serve. Sign in with your work email and your developer key plus test credits are issued instantly — you do not need to request access or be granted anything first. Head over to the [claim page](https://www.terminal3.io/claim-page) and sign in with your work email. claim-page-image Coming from an event or a campaign? Enter your campaign code to receive additional credits (the amount depends on the campaign). Once you submit the form, your developer key appears immediately. This is the key the SDK uses to authenticate as you — save it somewhere safe. During the test phase, the key is shown only once and can't be retrieved after you leave the page. Copy it before you navigate away. api-success-image Your unique T3N ID (`did:t3n`) and test credits are generated and linked to this key automatically. That's it — you're ready to build. Let's [set up your development environment](/developers/adk/get-started/prerequisites/set-up-dev-env), then follow the [walkthrough](/developers/adk/get-started/walkthrough/write-contract). If you encounter any issues during this process, please refer to [Common errors](/developers/adk/tips/common-errors) or do not hesitate to drop us an email at [devrel@terminal3.io](mailto:devrel@terminal3.io). # Set Up Development Environment Source: https://docs.terminal3.io/developers/adk/get-started/prerequisites/set-up-dev-env Two more steps to get ready for writing your first TEE contract. This page picks up where [Quickstart](/developers/adk/get-started/quickstart) leaves off. Complete that first — you should already have an authenticated `T3nClient` and your `tenantDid` before starting here. If you haven't already, get your DID, download your API key, and claim test credits from the [claim page](/developers/adk/get-started/prerequisites/request-test-tokens) — it's self-serve, no approval needed. [TEE contracts](/t3n/how-t3n-works/tees#tee-contract) are compiled to WebAssembly (WASM) binaries, built with the Rust toolchain. If you don't already have `rustup`/`cargo` installed: ```bash theme={null} curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # choose the default option when prompted source "$HOME/.cargo/env" ``` Then add the WASM target: ```bash theme={null} rustup target add wasm32-wasip2 # WASI Preview 2 build target — a few seconds cargo install wasm-tools # optional — inspect/verify the component ``` `cargo install wasm-tools` compiles roughly 100 crates from source and takes about 2 minutes with no progress output in between — that's normal, not a hang. Contracts are registered and managed through a `TenantClient`, built around the `tenantDid` you already obtained in Quickstart — never construct or derive this value yourself. Append this to the bottom of the same `quickstart.ts` you created in Quickstart — it needs `t3n` and `tenantDid` from that file's scope. See [Running the code samples in this guide](/developers/adk/tips/running-code-samples) for the `cat >> quickstart.ts << 'EOF'` pattern: ```bash theme={null} cat >> quickstart.ts << 'EOF' import { TenantClient, getNodeUrl } from "@terminal3/t3n-sdk"; const tenant = new TenantClient({ t3n, // the T3nClient you already authenticated in Quickstart baseUrl: getNodeUrl(), // the active node from setEnvironment() tenantDid, // did.value from Quickstart — never hardcode }); await tenant.tenant.me(); // throws if something's wrong; confirms the client actually works console.log("TenantClient ready."); EOF ``` **`me()` lives on `tenant.tenant`, not `tenant` directly.** `TenantClient` exposes `tenant`/`maps`/`contracts`/`token` as sub-namespaces — `me()` and `claim()` are on the `tenant` sub-namespace (`TenantNamespace`), so the call is `tenant.tenant.me()`. Run it the same way as before — `npx tsx quickstart.ts` — and confirm you see `TenantClient ready.` printed after your `tenantDid` line. **This authenticates you to manage your own deployment** — a different job from agent authentication. The DID must equal the one admitted as a tenant in `idx:_tenants`: exactly the `tenantDid` you already have from Quickstart. ## What's next Continue to [Write your first TEE contract](/developers/adk/get-started/walkthrough/write-contract). # Quickstart Source: https://docs.terminal3.io/developers/adk/get-started/quickstart Get your first authenticated call to Terminal 3 working in under 10 minutes. This page gets you to one working, authenticated call — nothing else. No Rust, no WASM, no blockchain knowledge required for this part. Once this works, move on to [Write your first TEE contract](/developers/adk/get-started/walkthrough/write-contract) for the real thing. Go to the [claim page](/developers/adk/get-started/prerequisites/request-test-tokens) and sign in. You get an API key and test credits instantly — no approval wait. Your key is shown **once**. Copy it somewhere safe before you navigate away — there's no way to view it again. Create a fresh project and install the SDK: ```bash theme={null} mkdir my-t3n-app && cd my-t3n-app npm init -y npm pkg set type=module # required — the code below uses top-level await npm install @terminal3/t3n-sdk tsx ``` **Don't pin an old version by habit.** `@terminal3/t3n-sdk` ships frequently (breaking grant-shape and other changes land in minor versions — see the [Changelog](/developers/adk/changelog)), so `npm install @terminal3/t3n-sdk` with no version suffix — always latest — is the right default here. Pin an exact version only once you've deliberately decided to hold back, and check the changelog when you do bump. Then put the key from Step 1 into your shell's environment — the code in the next step reads it from there, it isn't written into any file: ```bash theme={null} export T3N_API_KEY="" ``` A handful of teams have hit WASM-loading errors under Next.js/Turbopack, Vite, and older Webpack setups — the SDK loads a WASM component, and some bundlers try to process it in ways that break it. If you hit a WASM parse or module-loading error later in this guide and you're in a bundled framework, try running the SDK code from a plain Node script or a server-only route first, to confirm your setup works before debugging the bundler. If you're on Next.js specifically, adding your bundler's equivalent of an external-packages/no-bundle exception for `@terminal3/t3n-sdk` is worth trying. We're tracking this as a known rough edge — if you find a config that fixes it, tell us in the [developer Telegram](https://t.me/terminal3developer) and we'll document it here properly. Create a file named `quickstart.ts` in your project and paste this in. Don't paste the code directly at your shell prompt — see [Running the code samples in this guide](/developers/adk/tips/running-code-samples) for the `cat > quickstart.ts << 'EOF'` pattern this and every later step assumes: ```typescript theme={null} 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 (and switch to "production" when you go live) const T3N_API_KEY = process.env.T3N_API_KEY!; const wasmComponent = await loadWasmComponent(); // all crypto runs inside this component const address = eth_get_address(T3N_API_KEY); const t3n = new T3nClient({ // Verifies you're really talking to a genuine T3N enclave, not just // whatever the server claims — see "Verify the trust anchor" below. // Node URL comes from setEnvironment above. trustAnchor: await resolveTrustAnchor("testnet"), 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:... — you'll reuse this exact variable in every later step console.log("Connected as:", tenantDid); ``` **`trustAnchor` is required — `T3nClient` throws immediately if it's missing.** `resolveTrustAnchor(env)` is the right call for any hosted node; the only other valid value is the explicit opt-out `{ unsafe_trust_server: true }`, for local dev nodes only. See [Verify the trust anchor](/developers/adk/tips/verify-trust-anchor) for what it's actually checking. **Never hardcode or derive your tenant DID.** It's not related to your wallet address — it's an opaque ID the platform assigns you the first time you sign in. Always read it back from the authenticated session, like `tenantDid` above — and keep both `t3n` (the client) and `tenantDid` (the value) around, since [Set Up Dev Env](/developers/adk/get-started/prerequisites/set-up-dev-env) and the rest of the walkthrough build directly on top of them. **"Reuse this variable" means literally keep coding in this same file.** `t3n` and `tenantDid` are plain JavaScript variables — they only exist inside this one running script. The code samples on later pages (`TenantClient`, contract registration, invocation) assume `t3n`/`tenantDid`/`tenant` are already in scope, so append each new snippet to the bottom of this same `quickstart.ts` rather than starting a new file, unless you deliberately repeat the connect-and-authenticate block above at the top of it. `setEnvironment("testnet" | "production")` tells the SDK which T3N cluster to resolve a node URL from for every client you create afterward — you never hardcode a node URL yourself. Its public build defaults to `"testnet"`; setting it explicitly keeps your target cluster unambiguous and makes the switch to `"production"` a deliberate one-line change. From the same directory as `quickstart.ts`: ```bash theme={null} npx tsx quickstart.ts # Connected as: did:t3n:9f2a... ``` If you see a `did:t3n:...` value printed, you're connected. 1. Get your API key from the [claim page](/developers/adk/get-started/prerequisites/request-test-tokens) — shown once, copy it somewhere safe. 2. Save our [skill file](/developers/adk/support/ai-coding-assistants) as `.claude/skills/t3n-adk-quickstart/SKILL.md` in your project. 3. Describe what you're building. Claude Code scaffolds the project (`package.json`/ESM setup, your API key in the environment), writes and runs the connection code, and checks a known-pitfalls list before guessing at fixes. You should still end up with a working `t3n` client and `tenantDid` — the same things the manual path produces — just without typing the steps yourself. 1. Get your API key from the [claim page](/developers/adk/get-started/prerequisites/request-test-tokens) — shown once, copy it somewhere safe. 2. Save the same [skill file](/developers/adk/support/ai-coding-assistants) as `AGENTS.md` in your project root — Codex reads it automatically. 3. Describe what you're building. It follows the same order as the manual path: working connection first, contract code second. ## What's next You now have two things every later page assumes you already have: an authenticated `t3n` client and its `tenantDid`. Keep both around (same variable names, same file) as you continue: * Building something that actually runs inside the confidential-compute enclave → [Write your first TEE contract](/developers/adk/get-started/walkthrough/write-contract) * Letting an AI agent act on a user's behalf → [Member Delegation](/developers/adk/get-started/member-delegation). An agent needs its **own** DID and its **own** test credits — separate from yours, and from the same claim page you used in Step 1 — so get it its own key before writing any agent code: [Register a Public Agent](/developers/agents/register-agent) * Understanding what `resolveTrustAnchor` actually verifies → [Verify the trust anchor](/developers/adk/tips/verify-trust-anchor) * The shape of the whole ADK before you dive in further → [ADK Tour](/developers/adk/overview/adk-tour) * Hit an error? → [Common Errors](/developers/adk/tips/common-errors) # 2. Build your TEE contract Source: https://docs.terminal3.io/developers/adk/get-started/walkthrough/build-contract This step turns the Rust contract from [Step 1](/developers/adk/get-started/walkthrough/write-contract) into a WASM component. Run these commands below from the contract repository root, where `Cargo.toml` and `wit/world.wit` live. You do not need `cargo-component`. With `crate-type = ["cdylib", "lib"]` in `Cargo.toml`, the `wasm32-wasip2` target emits a WASM component that T3N can inspect and register. ## Build the release artifact Install the WASI Preview 2 target once per machine, then build the release artifact: ```bash theme={null} rustup target add wasm32-wasip2 cargo build --target wasm32-wasip2 --release ``` Cargo writes the component to `target/wasm32-wasip2/release/`. If your package name contains hyphens, Cargo converts them to underscores in the file name. The `z-tenant-flight` package therefore builds to: ```bash theme={null} target/wasm32-wasip2/release/z_tenant_flight.wasm ``` Confirm the file exists before moving on: ```bash theme={null} ls -lh target/wasm32-wasip2/release/*.wasm ``` The `.wasm` file is the artifact you pass to `tenant.contracts.register` in [Step 3](/developers/adk/get-started/walkthrough/register-contract). ## Verify the component interface Use `wasm-tools` to print the component's WIT interface: ```bash theme={null} wasm-tools component wit target/wasm32-wasip2/release/z_tenant_flight.wasm ``` The output should include the host interfaces you imported in `wit/world.wit`, such as `host:interfaces/kv-store`, and your exported interface: ```wit theme={null} export contracts; ``` If `wasm-tools` is not installed yet: ```bash theme={null} cargo install wasm-tools ``` # 4. Invoke your TEE contract Source: https://docs.terminal3.io/developers/adk/get-started/walkthrough/invoke-contract Agents call your contract via the same `execute` transport as any other T3N contract. The only difference is the `contract_id` starts with `z::`. **Agents authenticate as themselves, not as tenants.** Like every T3N session, an agent reads its own DID back from the authenticated session — there's nothing tenant-specific to set. ## 1. Set up the agent's identity An agent is a separate authenticated session with its own key — it is not the tenant and not the user. Build it the same way you built `t3n` in [Quickstart](/developers/adk/get-started/quickstart), but with its own credential: ```typescript theme={null} import { T3nClient, loadWasmComponent, fetchTrustedManifest, createEthAuthInput, eth_get_address, metamask_sign, getContractVersion, getNodeUrl, fetchTrustedManifest, } from "@terminal3/t3n-sdk"; const trustAnchor = await fetchTrustedManifest("testnet"); // required — see Verify the trust anchor; reused below for every client in this file const agentKey = process.env.AGENT_KEY!; // a separate credential — never reuse your tenant's T3N_API_KEY const agentAddress = eth_get_address(agentKey); const agentClient = new T3nClient({ trustAnchor: await fetchTrustedManifest("testnet"), wasmComponent, // node URL resolved from setEnvironment() — see set-up-dev-env trustAnchor, handlers: { EthSign: metamask_sign(agentAddress, undefined, agentKey), }, }); await agentClient.handshake(); const agentAuth = await agentClient.authenticate(createEthAuthInput(agentAddress)); const agentDid = agentAuth.value; // reused below when the user authorizes this agent const TENANT_CONTRACT = `z:${tenantDid.slice("did:t3n:".length)}:travel-contracts`; const contractVersion = await getContractVersion(getNodeUrl(), TENANT_CONTRACT); ``` **Where `AGENT_KEY` comes from.** An agent's key is not derived from your tenant key — get it the same way you got your own, from the [claim page](/developers/adk/get-started/prerequisites/request-test-tokens). It needs its **own** test credits too: an agent DID's balance is separate from its tenant's and starts at zero, so a key generated any other way (or your tenant's key, reused) will fail metered calls with `InsufficientCreditError`. See [Register a Public Agent](/developers/agents/register-agent) for the full flow. ## 2. Authorize the contract's egress (as the user) Before any function that makes an outbound HTTP call can run, the **user (data owner)** must authorize it. A tenant contract's allowed hosts are resolved per-call from the user's authorization grant — not from the contract. In this walkthrough you're standing in for the user yourself, so build a third session — `userClient` — with its own credential, exactly like `t3n` and `agentClient` above: ```typescript theme={null} const userKey = process.env.USER_KEY!; // stands in for the real data owner's own key const userAddress = eth_get_address(userKey); const userClient = new T3nClient({ trustAnchor: await fetchTrustedManifest("testnet"), wasmComponent, trustAnchor, // same anchor as agentClient above — one environment, one manifest handlers: { EthSign: metamask_sign(userAddress, undefined, userKey), }, }); await userClient.handshake(); await userClient.authenticate(createEthAuthInput(userAddress)); ``` Now the user signs a `member-delegation-update` scoping the agent to your contract, its functions, and the hosts it may reach: ```typescript theme={null} // Signed by the USER (data owner), not the agent. const userContractVersion = await getContractVersion(getNodeUrl(), "tee:user/contracts"); await userClient.execute({ contract_id: "tee:user/contracts", contract_version: userContractVersion, function_name: "member-delegation-update", input: { grants: [{ grantee: agentDid, // from step 1 contract_id: TENANT_CONTRACT, // z::travel-contracts, from step 1 version_req: contractVersion, functions: ["search-offers", "book-offer"], allowed_hosts: ["api.duffel.com"], // hosts the contract may dial }], }, }); ``` `member-delegation-update` **replaces the member's whole delegation policy.** This raw call is fine here — it's the first grant — but a later single-grant write would drop the earlier ones. To add grants incrementally, call `userClient.updateMemberDelegation(grant)`, which read-merge-writes so existing grants survive. See [Member Delegation](/developers/adk/get-started/member-delegation). For a **direct (self) call** — where the user invokes the contract themselves rather than through a separate agent — set `grantee` to the user's own DID (a self-grant) instead of building a separate `agentClient`. Without a matching grant the contract still runs, but any outbound call is denied with `host/http.egress_denied`. See [Outbound HTTP is authorized by the user, not the contract](/developers/adk/tips/outbound-http-auth-by-user). ## 3. Invoke your contract (as the agent) With the grant in place, the agent can call the contract's functions: ```typescript theme={null} // 1. Search for offers (no PII) const search = await agentClient.executeAndDecode({ contract_id: TENANT_CONTRACT, contract_version: contractVersion, function_name: "search-offers", input: { origin: "LHR", destination: "JFK", departure_date: "2026-07-15", cabin_class: "economy", adult_count: 1 }, }); const offer = search.offers[0]; // 2. Book the chosen offer. No PII in the input — name, DOB and email are // resolved host-side from the user's profile via http-with-placeholders, // and only when the user's grant authorizes this agent (see the grant above). const booking = await agentClient.executeAndDecode({ contract_id: TENANT_CONTRACT, contract_version: contractVersion, function_name: "book-offer", input: { offer_id: offer.id, passenger_id: offer.passenger_ids[0], // opaque Duffel id from search — not PII total_amount: offer.total_amount, total_currency: offer.total_currency, }, }); // booking.pnr → the flight booking reference. The passenger's name never left the enclave. ``` # 3. Register your TEE contract Source: https://docs.terminal3.io/developers/adk/get-started/walkthrough/register-contract Registration uploads the WASM component you built in [Step 2](/developers/adk/get-started/walkthrough/build-contract) and gives it a tenant-local name. After this step, T3N knows about your contract and gives you a numeric `contract_id` that you use when creating map ACLs. This code goes back in `quickstart.ts` — the Node/TypeScript project from Quickstart — not in the `z-tenant-flight` Rust repo. Append it to the bottom, after the `TenantClient` code from [Set Up Dev Env](/developers/adk/get-started/prerequisites/set-up-dev-env), using the `cat >> quickstart.ts << 'EOF'` pattern from [Running the code samples in this guide](/developers/adk/tips/running-code-samples). Before you run this code, make sure you have: * An authenticated `TenantClient` named `tenant`. If you have not created one yet, complete [Quickstart](/developers/adk/get-started/quickstart) and [Set Up Dev Env](/developers/adk/get-started/prerequisites/set-up-dev-env) first. * A compiled WASM file at `target/wasm32-wasip2/release/z_tenant_flight.wasm`, **inside the separate `z-tenant-flight` folder you cloned** in [Write your TEE contract](/developers/adk/get-started/walkthrough/write-contract) — not inside your Node project. * Your `tenantDid`, for example `did:t3n:abcdef0123456789abcdef0123456789abcdef01`. ## Choose a contract tail The `tail` is the local name of your contract inside your tenant namespace. Pass only the part after `z::`. For example, the tail `travel-contracts` becomes: ```text theme={null} z::travel-contracts ``` Do not include `z::` in the `tail`; the SDK and host derive that from the authenticated tenant. A tail may contain letters, digits, `_`, `-`, and `.` — but **not** `/`. The SDK rejects slashes (`tail must match /^[a-zA-Z0-9_-][a-zA-Z0-9_.-]{0,127}$/`). Pick a stable tail for each contract you plan to maintain. When you register a new build at the same tail, increase the `version` value; changing the tail creates a separate contract entry. Keep tails short and simple (a few words, hyphen-separated) rather than long or descriptive. The full canonical name (`z::`) gets reused downstream in places like delegation grants, and a handful of teams have hit unexpectedly-strict length limits further down the pipeline when using long tails. The 128-character limit above is enforced at registration; treat it as a ceiling, not a target. ## Register the WASM ```typescript theme={null} import { readFile } from "fs/promises"; // Path to the .wasm file INSIDE your cloned z-tenant-flight folder, relative // to quickstart.ts. Adjust this if you cloned it somewhere else. const WASM_PATH = "../z-tenant-flight/target/wasm32-wasip2/release/z_tenant_flight.wasm"; const CONTRACT_TAIL = "travel-contracts"; const CONTRACT_VERSION = "0.1.0"; const wasmBytes = await readFile(WASM_PATH); const result = await tenant.contracts.register({ tail: CONTRACT_TAIL, version: CONTRACT_VERSION, wasm: wasmBytes, }); // This numeric ID is required in the next setup step when you create map ACLs. const contractId = result.contract_id; const tenantId = tenantDid.slice("did:t3n:".length); const contractName = `z:${tenantId}:${CONTRACT_TAIL}`; console.log(`registered ${contractName} as contract id ${contractId}`); ``` `WASM_PATH` above assumes you cloned `z-tenant-flight` as a sibling folder next to your Node project (i.e. `../z-tenant-flight/...` from `quickstart.ts`) — if you put it somewhere else, update the path accordingly. Registration does not run your code, create maps, seed secrets, or grant outbound HTTP access. It only stores the component and records the versioned contract entry for your tenant. ## What T3N stores The register payload is just `{ tail, version, wasm }`; there is no manifest. Host-side, T3N: 1. Stores the WASM blob in content-addressed storage. 2. Allocates a numeric `ContractId`. 3. Records the contract under your tenant registry. Your contract's capabilities come from the host interfaces it imports in `world.wit`, not from this registration request. See [Capabilities come from your WIT imports](/developers/adk/tips/capabilities-from-wit-import). Outbound hosts are also not declared here. They come from the calling user's authorization grant at invoke time. See [Outbound HTTP is authorized by the user, not the contract](/developers/adk/tips/outbound-http-auth-by-user). **Re-registering a tail allocates a new `contract_id`.** Unpinned calls resolve to the *latest* registered version (that's what `getContractVersion()` returns); if you need a specific version, pass it explicitly to `contracts.execute()` and it will be honored. The real gotcha is the id: there is currently no API to fetch a tail's current `contract_id` after re-registering, so if you created map ACLs scoped to the old `contract_id`, a re-registration can leave them pointing at a stale id. Keep a record of each `contract_id` your tenant registers so you can re-grant map access if needed. ## First-run troubleshooting | Error or symptom | What it usually means | What to do | | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ENOENT: no such file or directory` | The WASM path is wrong, or the contract was not built yet — a common cause is `WASM_PATH` pointing into your Node project instead of across to the separate `z-tenant-flight` folder. | Re-run Step 2 and confirm the path with `ls -lh /target/wasm32-wasip2/release/*.wasm`, then check `WASM_PATH` is relative to where `quickstart.ts` actually runs from. | | `tenant not found` | The session DID does not match an admitted tenant — you constructed or derived `tenantDid` instead of reading it from the session. | Read `tenantDid` from `did.value` after authenticating (see [Quickstart](/developers/adk/get-started/quickstart)), then rebuild `TenantClient` with it. Confirm with `tenant.tenant.me()`. | | `version is not higher than current version ` | You already registered this tail with the same or a higher version. | Bump `CONTRACT_VERSION`, for example from `0.1.0` to `0.1.1`. | | The contract registers, but later cannot read `secrets` | The map does not exist yet, or its ACL does not include this `contractId`. | Use the returned `contractId` when creating the `secrets` map ACLs. | | A previously-working pinned-version call starts failing or behaving unexpectedly | An explicit pinned version is honored by `contracts.execute()`, so a pinned call failing usually means that version was never successfully registered/executable — not that it was shadowed. | Verify the pinned version is actually registered and executable; for the default (unpinned) path, `getContractVersion()` returns the latest registered version. | | A long contract tail is rejected further downstream (e.g. when building a delegation grant), even though registration succeeded | Some downstream operations enforce a shorter length limit on the full canonical name than registration does. | Prefer a short tail from the start — see the note above. | The contract is now registered. It still cannot complete the full end-to-end flow until the [maps](/developers/adk/tips/create-kv-maps) and [secrets](/developers/adk/tips/seed-api-key) it reads at runtime exist. # 5. Test your TEE contract Source: https://docs.terminal3.io/developers/adk/get-started/walkthrough/test **Unit tests (Rust):** Test the business-logic guards on the native target with `cargo test`. Each function takes the raw input bytes and returns `Result, String>`, so you call it directly and assert on the error string — there is no `ContractError` enum or test harness to set up. Functions that call the host (`http`, `kv-store`) only run on `wasm32`; natively they return an error, so unit tests focus on input parsing and validation. ```rust theme={null} #[cfg(test)] mod tests { use super::*; #[test] fn book_offer_rejects_inline_pii() { // book-offer deserialises into a struct with only opaque ids + amount, // so a payload that tries to smuggle passenger PII fails at parse time. let input = serde_json::to_vec(&serde_json::json!({ "offer_id": "off_1", "passengers": [{ "given_name": "Jane" }], // not a valid field "total_amount": "199.00", "total_currency": "GBP", })) .unwrap(); let err = book_offer(&input).unwrap_err(); assert!(err.contains("bad input")); } #[test] fn book_offer_rejects_non_json() { assert!(book_offer(b"not json").unwrap_err().contains("bad input")); } } ``` **Test checklist:** * Happy path: `search-offers` → `book-offer` returns `{ id, pnr, status }`. * Input hygiene: `book-offer` accepts only `offer_id`, `passenger_id`, `total_amount`, `total_currency` — any payload carrying passenger PII is rejected. * PII never in output: `passport`, `date_of_birth`, and name fields do not appear in any return value or log line. * The `{{profile.*}}` markers stay literal in the contract — resolution happens host-side, so they never appear resolved in WASM memory. # What is z-namespace? Source: https://docs.terminal3.io/developers/adk/get-started/walkthrough/what-is-z-namespace Before you write a contract, it's worth knowing where your data actually lives. This page covers that in two minutes; the [next page](/developers/adk/get-started/walkthrough/write-contract) starts the real walkthrough, using `Terminal-3/z-tenant-flight` — a Duffel flight booking contract — as a worked example throughout. ## What is z-namespace? The `z:` namespace is T3N's tenant data layer. Every map and every registered contract you create as a tenant lives under `z::…`, where `` is the 20-byte lowercase hex suffix of your tenant DID. ``` ALL MAPS │ ├── AppKvMap (system data — T3N engineers only) │ ├── users, auth, dids, scripts, outbox_*… │ └── idx:_tenants, idx:_tenant_contracts… │ └── z:: (your data — you own this) ├── z::secrets ← your Duffel API key, sealed inside the enclave └── z::anything ← DENIED (cross-tenant isolation enforced at every KV op) ``` The cluster enforces one rule at hardware level: a contract can only read or write maps whose prefix matches its own ``. There is no ACL misconfiguration that can break this — the check runs inside the TDX enclave at every transaction. **What T3N guarantees:** * Your maps are inaccessible to other tenants and to system contracts. * User PII (`users` map) is unreachable to your contract's direct reads — PII flows through the `http-with-placeholders` host interface, which substitutes it into outbound requests inside the enclave (see [Placeholders in outbound calls](/developers/adk/tips/placeholders-outbound-calls)). * Every operation your contract performs lands in the cluster's append-only audit log with your tenant DID as a first-class field — see [Read your organization's activity log](/developers/adk/tips/get-activity-log) for how to query it. For the full access-control model (readers/writers ACLs per map), see [Storage Namespaces](/t3n/how-t3n-works/z-namespace). # 1. Write your TEE contract Source: https://docs.terminal3.io/developers/adk/get-started/walkthrough/write-contract Clone the reference implementation now rather than typing the Rust code by hand — it's a **separate project** from the Node/TypeScript app you built in Quickstart, so put it in its own folder alongside it, not inside it: ```bash theme={null} cd .. # out of my-t3n-app, back to a shared parent folder git clone https://github.com/Terminal-3/z-tenant-flight.git cd z-tenant-flight ``` Below is a walkthrough of the pieces inside that repo — change the host calls and flight-specific logic to match your needs once you understand them. A TEE contract is a Rust crate compiled to a WASM **component**. It exports its functions through a `contracts` WIT interface and imports only the host capabilities it needs. Key concepts and tips before starting: * [Storage namespace](/t3n/how-t3n-works/z-namespace) * [Host API](/t3n/how-t3n-works/host-api) * [Create Tenant KV maps](/developers/adk/tips/create-kv-maps) * [Capabilities come from your WIT imports](/developers/adk/tips/capabilities-from-wit-import) ## Repository Structure ``` z-tenant-flight/ ├── src/ │ ├── lib.rs ← wit-bindgen entry point + Guest impl that dispatches to each fn │ ├── search.rs ← search-offers — Duffel search (no PII) │ └── booking.rs ← book-offer — Duffel booking (PII via http-with-placeholders) ├── wit/ │ ├── world.wit ← the world your contract exports + the host interfaces it imports │ └── deps/ ← vendored host interface packages (host-interfaces, host-tenant) └── Cargo.toml ``` The packages under `wit/deps/` define the host ABI your contract links against — vendor the versions your target cluster provides (here, `host-interfaces-2.2.0/` and `host-tenant-1.2.0/`). ## Files ### world.wit — declare your interface + host imports ```wit theme={null} package z:tenant-flight@0.4.0; world tenant-flight { import host:tenant/tenant-context@1.2.0; import host:interfaces/logging@2.2.0; import host:interfaces/kv-store@2.2.0; import host:interfaces/http@2.2.0; // search (no PII) import host:interfaces/http-with-placeholders@2.2.0; // booking (PII via placeholders) export contracts; } interface contracts { // Uniform 3-field envelope used by every node-callable contract. // input — JSON arguments for this function, as bytes // user-profile — None for tenant contracts (profile is resolved host-side) // context — node-minted DynamicContext (trusted), as bytes record generic-input { input: option>, user-profile: option>, context: option>, } // One func per operation. Each takes generic-input and returns JSON bytes on // success, or an error string. There is no central `dispatch` function and no // `ContractError` enum — the function name *is* the export. search-offers: func(req: generic-input) -> result, string>; book-offer: func(req: generic-input) -> result, string>; } ``` [The interfaces you import here are your contract's entire capability set](/developers/adk/tips/capabilities-from-wit-import) — there is no separate manifest. The host links your contract against the matching tenant world and refuses to load it if it imports an interface that world does not provide. ### Cargo.toml — compile to a WASM component ```toml theme={null} [package] name = "z-tenant-flight" version = "0.4.1" edition = "2021" # crate-type cdylib is what makes the wasm32-wasip2 target emit a WASM # *component* (not a bare module). Keep "lib" too so the business logic # stays unit-testable natively. [lib] crate-type = ["cdylib", "lib"] [dependencies] # wit-bindgen's macro generates the bindings from wit/ at compile time. wit-bindgen = { version = "0.49", default-features = false, features = ["macros", "realloc"] } serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } # Small, self-contained artifact — keeps registration under the size cap. [profile.release] opt-level = "s" lto = true codegen-units = 1 strip = true ``` ### lib.rs — generate bindings + dispatch to each function ```rust theme={null} wit_bindgen::generate!({ world: "tenant-flight", path: "wit", additional_derives: [ serde::Deserialize, serde::Serialize, ], generate_all, }); mod booking; mod search; struct Component; // Implement the exported `contracts` interface. Each generated method unwraps // the input bytes and hands off to the module that does the work. #[cfg(target_arch = "wasm32")] impl exports::z::tenant_flight::contracts::Guest for Component { fn search_offers(req: exports::z::tenant_flight::contracts::GenericInput) -> Result, String> { let input = req.input.ok_or("search-offers: missing input")?; search::search_offers(&input) } fn book_offer(req: exports::z::tenant_flight::contracts::GenericInput) -> Result, String> { let input = req.input.ok_or("book-offer: missing input")?; booking::book_offer(&input) } } #[cfg(target_arch = "wasm32")] export!(Component); ``` The host bindings live under `crate::host::*` and the exported interface under `crate::exports::*` — both generated by the macro from `wit/`. ### search.rs — `search_offers` (synchronous `http`, no PII) The `http` interface is synchronous: the response is available before the call returns. Build a `Request` with a `Verb`, headers, and an optional payload. ```rust theme={null} use crate::host::interfaces::{http as http_iface, logging}; let resp = http_iface::call(&http_iface::Request { method: http_iface::Verb::Post, url: format!("{DUFFEL_BASE}/air/offer_requests?return_offers=false"), headers: Some(duffel_headers(&api_key)), // Vec<(String, String)> payload: Some(serde_json::to_vec(&offer_request_body).map_err(|e| e.to_string())?), }) .map_err(|e| format!("duffel offer-request: {e}"))?; if resp.code != 201 { let body = String::from_utf8_lossy(&resp.payload); return Err(format!("Duffel offer-request failed: HTTP {} — {body}", resp.code)); } let _ = logging::info("offer request created"); // resp.payload holds the response bytes — parse with serde_json. ``` [Outbound HTTP is authorized by the user, not the contract](/developers/adk/tips/outbound-http-auth-by-user) — the hosts a contract may reach are resolved per-call from the calling user's grant. ### booking.rs — `book_offer` (PII via `http-with-placeholders`) For calls that carry user PII, use `http-with-placeholders`. Put `{{profile.}}` markers in the request body; the host resolves them from the calling user's profile at dispatch time, so plaintext PII never enters WASM memory. ```rust theme={null} use crate::host::interfaces::http_with_placeholders as hwp; use serde_json::json; let order_body = json!({ "data": { "type": "instant", "selected_offers": [req.offer_id], "passengers": [{ "id": req.passenger_id, // opaque Duffel id — not PII // Resolved host-side from the user's profile (PII never enters WASM): "given_name": "{{profile.first_name}}", "family_name": "{{profile.last_name}}", "born_on": "{{profile.date_of_birth}}", "email": "{{profile.verified_contacts.email.value}}", }], "payments": [{ "type": "balance", "amount": req.total_amount, "currency": req.total_currency }] } }); let resp = hwp::call(&hwp::Request { method: hwp::Verb::Post, url: format!("{DUFFEL_BASE}/air/orders"), headers: Some(duffel_headers(&api_key)), payload: Some(serde_json::to_vec(&order_body).map_err(|e| e.to_string())?), }) .map_err(|e| format!("duffel create-order: {}", format_http_error(e)))?; ``` `hwp::call` returns a typed `HttpError` so failures never leak resolved PII — match on it for clear messages: ```rust theme={null} fn format_http_error(e: hwp::HttpError) -> String { match e { hwp::HttpError::EgressDenied(host) => format!("egress denied for host {host}"), hwp::HttpError::PlaceholderDenied(marker) => format!("placeholder not permitted: {marker}"), hwp::HttpError::PlaceholderUnknown(field) => format!("user profile missing field: {field}"), hwp::HttpError::PlaceholderNoUserContext => "no user context bound for placeholder resolution".to_string(), hwp::HttpError::UpstreamError(reason) => format!("upstream: {reason}"), } } ``` See [Placeholders in outbound calls](/developers/adk/tips/placeholders-outbound-calls). ### Reading secrets from the `secrets` KV map The API key is read from the tenant's `secrets` KV map at runtime. [The key is seeded by the tenant SDK](/developers/adk/tips/seed-api-key) before the contract runs — there is no `set-credentials` host function. `kv-store` calls take the **full** `z::` name; build it from `tenant-context` at runtime (the host enforces the prefix): ```rust theme={null} use crate::host::{interfaces::kv_store, tenant::tenant_context}; fn get_api_key() -> Result { // tenant_did() returns raw bytes (list) — hex-encode them to build the // z:: map path (this matches the map the tenant SDK created for you). let tid = tenant_context::tenant_did(); let map_name = format!("z:{}:secrets", hex::encode(&tid)); let bytes = kv_store::get(&map_name, b"duffel_api_key") .map_err(|e| format!("kv read: {e}"))? .ok_or("duffel_api_key not found in z::secrets — populate it via the tenant SDK before use")?; String::from_utf8(bytes).map_err(|e| e.to_string()) } ``` ## Key Design Rules * Export functions on the `contracts` interface. Each takes `generic-input` and returns `result, string>` — JSON bytes on success, an error string on failure. There is **no** `dispatch` function and **no** `ContractError` enum. * `kv-store` calls take the **full** `z::` name. Build it at runtime by hex-encoding `tenant_context::tenant_did()`, which returns raw bytes: `format!("z:{}:secrets", hex::encode(&tid))`. The host enforces the prefix. The map must exist (created and populated by the tenant SDK) before the contract reads or writes it. * Import only the host interfaces you use — they are your contract's entire capability set. The host refuses to load a contract that imports an interface its tenant world does not provide. * `http::call` is synchronous; you get the response back before the function returns. Its egress is authorized per-call by the calling user's grant. * For calls carrying user PII, use `http-with-placeholders`: put `{{profile.}}` markers in the request and the host resolves them inside the enclave, so plaintext PII never enters your contract. # ADK Tour Source: https://docs.terminal3.io/developers/adk/overview/adk-tour The shape of the ADK in five pieces, before you write any code. If you're new here, this page is the map. It won't teach you to build anything — [Quickstart](/developers/adk/get-started/quickstart) does that in under 10 minutes — but it'll save you from guessing what each piece is for while you read the rest of the docs. The ADK is built around five ideas. Everything else in these docs is a detail of one of these. When you sign in and get an API key, T3N gives you a **tenant identity** — an opaque ID (`did:t3n:...`) that everything you own is scoped to: your data, your contracts, your credits. It's not derived from your wallet, and you never construct it yourself — you always read it back from an authenticated session. Anything you store (an API key, a config value, application state) goes into a **tenant KV map**, namespaced under your tenant ID so no other tenant can ever read or write it. Access within your own maps is opt-in — you explicitly say what a contract is allowed to touch. See [Create Tenant KV Maps](/developers/adk/tips/create-kv-maps). A **TEE contract** is a small Rust program, compiled to WebAssembly, that runs inside confidential-computing hardware (a TEE). The point of running it there instead of on your own server: the contract can process user PII and call third-party APIs on a user's behalf, without your infrastructure — or you — ever seeing the plaintext. See [Write your first TEE contract](/developers/adk/get-started/walkthrough/write-contract). An AI agent doesn't get standing access to anything. A user (or you, acting on your own tenant) explicitly grants an agent permission to call specific functions on a specific contract, optionally scoped to specific external hosts it's allowed to reach. No grant, no access — the contract still runs, the outbound call just gets denied. See [Member Delegation](/developers/adk/get-started/member-delegation). When a contract needs to send a user's real name, email, or other personal data to a third-party API, it doesn't handle that data directly. It sends a request with `{{profile.field}}` placeholder markers, and the host substitutes the real values inside the enclave at the last moment. Your contract — and anything logging or inspecting it — only ever sees the placeholder. See [Placeholders in outbound calls](/developers/adk/tips/placeholders-outbound-calls). ## Where to go next Get an authenticated call working in under 10 minutes. The full walkthrough, with a real worked example. How a member delegates scoped permission to an agent, and how agents authenticate. Bookmark this one — you'll want it. # What is T3 Agent Developer Kit (ADK)? Source: https://docs.terminal3.io/developers/adk/overview/what-is-adk An SDK suite that allows developers to build safe and secure AI agents ## Overview T3 Agent Developer Kit (ADK) is a client SDK that allows developers to build agent tenant applications on the [T3 Network (T3N)](/t3n/overview/what-is-t3n). It lets developers onboard an agent tenant identity, manage tenant-scoped data and [TEE contracts](/t3n/how-t3n-works/tees#tee-contract), and execute TEE contracts inside T3N. The current SDK only supports **TypeScript** / **JavaScript**. Support for more languages is coming soon. ## Key Capabilities | Capability | What it does | | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Authenticated session | `T3nClient.authenticate()` signs in with your Ethereum wallet and opens an encrypted channel to the [TEE node](/t3n/how-t3n-works/tees) — your `tenantDid` comes back from this call, never derived by hand. | | Tenant setup (`TenantClient`) | Built around your already-authenticated `tenantDid`; `tenant.tenant.me()` confirms it's wired up correctly. | | Tenant data (`tenant.maps`) | `tenant.maps.create()` creates key-value maps under your private `z::…` prefix, with explicit `readers`/`writers` access rules per map. | | Tenant contracts (`tenant.contracts`) | `tenant.contracts.register()` uploads a Rust→WASM contract and gives it a tenant-local name; registering a new `version` at the same tail allocates a new `contract_id` rather than replacing the old one. | | Invoking a contract | An authenticated agent or user client calls `executeAndDecode()` (or `execute()`) with `contract_id`/`contract_version`/`function_name`/`input` — the same pattern whether you're calling as a tenant, agent, or user. | | Hardware-enforced isolation | Every read and write is checked against your tenant prefix inside T3N — no ACL to misconfigure. | See the [Quickstart](/developers/adk/get-started/quickstart) and [Reference](/developers/adk/reference) for the exact method signatures — this page is a conceptual map, not the API surface itself. # Why T3 ADK? Source: https://docs.terminal3.io/developers/adk/overview/why-adk Give AI agents real-world capabilities without compromising user privacy, security and compliance T3 Agent Developer Kit (ADK) helps developers build AI agents that can securely identify themselves, access user-authorized data, and perform real-world actions on behalf of users—without exposing sensitive information to the model, application, or agent runtime. With the ADK, developers can: * **Build faster** using pre-built infrastructure for identity, confidential computing, secure data access, and agent governance. * **Protect user privacy by design** by keeping sensitive data out of prompts, context windows, and application servers (see [Placeholders in outbound calls](/developers/adk/tips/placeholders-outbound-calls)). * **Reduce compliance burden** with architecture designed to support privacy and regulatory requirements such as GDPR, and reuse of verified KYC/KYB credentials across integrations instead of repeating verification for every counterparty (see [Reusable Verified User Data](/t3n/use-cases/reusable-user-data)). * **Enable trusted agent actions** including transactions, approvals, and interactions with external services using verifiable permissions and a tamper-evident audit trail you can hand to a compliance team or an auditor (see [Read Your Organization's Activity Log](/developers/adk/tips/get-activity-log)). * **Connect to a network of users and agents** through portable identities and verifiable credentials that work across applications and ecosystems. # SDK & API Reference Source: https://docs.terminal3.io/developers/adk/reference Every confirmed ADK method, WIT host interface, and API surface in one place. This page collects everything scattered across the walkthrough and tips pages into one lookup table. If you already know the name of what you're looking for, start here before searching the guides. Every row below is confirmed against this documentation or the OpenAPI spec. Anything only seen in community code (not in official docs) is called out separately in the last section, with a warning — not stated as fact. ## ADK client SDK (`@terminal3/t3n-sdk`) | Symbol | What it does | Where it's covered | | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `T3nClient` | Low-level client — handles the encrypted session, SIWE auth, and `execute` transport. | [Set Up Development Environment](/developers/adk/get-started/prerequisites/set-up-dev-env) | | `TenantClient` | Tenant-scoped client, built around your authenticated `tenantDid`. | [Set Up Development Environment](/developers/adk/get-started/prerequisites/set-up-dev-env) | | `setEnvironment("sandbox" \| "testnet" \| "production")` | Selects which T3N cluster every client resolves its node URL from. Defaults to `"testnet"` in the public build; `sandbox` is an alias of `testnet`. | [Quickstart](/developers/adk/get-started/quickstart) | | `loadWasmComponent()` | Loads the WASM component all client-side crypto runs inside. | [Quickstart](/developers/adk/get-started/quickstart) | | `eth_get_address(key)` | Derives an Ethereum address from a key for SIWE-style auth. | [Quickstart](/developers/adk/get-started/quickstart) | | `metamask_sign(address, _, key)` | `EthSign` handler — signs the login challenge. | [Quickstart](/developers/adk/get-started/quickstart) | | `createEthAuthInput(address)` | Builds the input for `authenticate()`. | [Quickstart](/developers/adk/get-started/quickstart) | | `fetchTrustedManifest(env, opts?)` | Fetches and signature-verifies the environment's trust manifest, returning a `TrustAnchor`. Required on every `T3nClient` — the constructor throws without it. | [Verify the trust anchor](/developers/adk/tips/verify-trust-anchor) | | `resolveTrustAnchor(env, opts?)` | One-call convenience over `fetchTrustedManifest` — resolves straight to a ready-to-use `TrustAnchor` for `T3nClient`, same verification, less boilerplate for the common case. Vendor-aware (TDX + AMD SEV-SNP) since `testnet-v1.0.10`. | [Verify the trust anchor](/developers/adk/tips/verify-trust-anchor) | | `verifyDkgAttestation(...)` | Lower-level, vendor-aware attestation check (dispatches to TDX or SEV-SNP verification based on the quote). Most integrations use `resolveTrustAnchor`/`T3nClient` instead of calling this directly. | [Verify the trust anchor](/developers/adk/tips/verify-trust-anchor) | | `verifyTdxQuote(...)` / `verifySevSnpReport(...)` / `detectQuoteVendor(...)` / `sevSnpVcekUrl(...)` / `fetchSevSnpCollateral(...)` | Vendor-specific attestation primitives underneath `verifyDkgAttestation`. Relevant mainly for a private-cloud cluster on SEV-SNP hardware. | [Verify the trust anchor](/developers/adk/tips/verify-trust-anchor) | | `client.handshake()` | Opens the encrypted session. Must be called before `authenticate()`. | [Quickstart](/developers/adk/get-started/quickstart) | | `client.authenticate(input)` | Authenticates and returns your DID — always read `did.value` rather than constructing it yourself. | [Quickstart](/developers/adk/get-started/quickstart) | | `tenant.tenant.me()` | Returns the authenticated tenant's session info, including `tenantDid`. | [Set Up Development Environment](/developers/adk/get-started/prerequisites/set-up-dev-env) | | `tenant.maps.create({ tail, visibility, readers, writers })` | Creates a tenant KV map. `readers`/`writers` default to deny — set explicitly. | [Create Tenant KV Maps](/developers/adk/tips/create-kv-maps) | | `tenant.maps.update(...)` | Updates an existing map's ACL (e.g. to add a `contractId`). | [Common Errors](/developers/adk/tips/common-errors) | | `tenant.maps.delete(tail)`, `tenant.maps.entrySet(...)`, `tenant.maps.entryGet(...)`, `tenant.maps.getStatus(...)` | Delete a map; seed/read a single entry from outside a contract; check a map's status. | [Seed API key into secrets map](/developers/adk/tips/seed-api-key) | | `tenant.contracts.register({ tail, version, wasm })` | Registers a compiled WASM contract under a tenant-local name. | [Register your TEE contract](/developers/adk/get-started/walkthrough/register-contract) | | `tenant.contracts.execute(...)` | Invokes a registered contract, returning the raw JSON-RPC result. | [Invoke your contract](/developers/adk/get-started/walkthrough/invoke-contract) | | `client.executeAndDecode(...)` | Same dispatch as `execute`, but JSON-decodes the response for you. Lives on the base `T3nClient`, not `tenant.contracts`. | [Invoke your contract](/developers/adk/get-started/walkthrough/invoke-contract) | | `member-delegation-update` (contract call, signed by the data owner) | Grants an agent access to specific functions on a specific contract, scoped to specific hosts. Read the policy back with `member-delegation-get`. | [Member Delegation](/developers/adk/get-started/member-delegation) | | `client.updateMemberDelegation(grant \| grant[], options?)` | The SDK write for `member-delegation-update`. One grant confers exactly one `function` (since `testnet-v1.0.11`, `@terminal3/t3n-sdk` 5.19.0) — pass an array, or build one with `fanOutGrant`, to authorise several. | [Member Delegation](/developers/adk/get-started/member-delegation) | | `fanOutGrant(base, functions)` / `readOnlyScopes(paths)` | Grant-building helpers: fan a shared grant base out into one row per function; build a read-only `scopes` list from bare paths. | [Member Delegation](/developers/adk/get-started/member-delegation) | | `getContractVersion(nodeUrl, contractId)` | Looks up the currently registered version of a contract. | [Invoke your contract](/developers/adk/get-started/walkthrough/invoke-contract) | | `client.getAuditEvents(opts?)` | Reads a page of the caller's own audit events (`pii_did`, `limit`, `cursor`). | [Changelog](/developers/adk/changelog) | | `client.getActivityLog(opts?)` | Reads a page of the caller's organisation's agent-activity audit log — every member's activity for an org admin, only the caller's own (and their agents') otherwise. Page with `before_seq` until `next_seq` is `null`. | [Changelog](/developers/adk/changelog) | | `client.exportActivityLog(opts?, poll?)` | Exports `getActivityLog`'s scope/filters to CSV, walked to exhaustion: starts the export, polls, and returns the finished file. One export at a time per caller. | [Changelog](/developers/adk/changelog) | | `client.startActivityLogExport(opts?)` / `getActivityLogExportStatus(exportId)` / `downloadActivityLogExport(exportId)` | The three calls `exportActivityLog` composes — use these instead when a UI must own its own polling (e.g. to survive a page reload). | [Changelog](/developers/adk/changelog) | | `invoke(opts)` | Stateless, single-request agent auth over `POST /api/invoke` — no handshake, no session. | [Member Delegation](/developers/adk/get-started/member-delegation) | | `client.createAgent(orgDid, name, options?)` | Mints an org-owned agent DID and (optionally, atomically) hosts its card. `options.initialCredits` pre-funds the new agent from the organisation's own balance in the same transaction — otherwise the agent starts with **zero** credit. Needs `tee:organisation/contracts` ≥ 0.7.0 for `initialCredits`. | [Register an Organization-owned Agent](/developers/agents/provision-org-agent), [Changelog](/developers/adk/changelog) | | `client.fundAgent(agentDid, amount, options?)` | Tops up an agent's T3N credit balance any time after creation — from the caller's own balance by default, or from `options.fromOrg`'s balance (requires admin of `fromOrg`, and `agentDid` must be a member of it). Returns `{ seqNo }`. | [Register an Organization-owned Agent](/developers/agents/provision-org-agent), [Changelog](/developers/adk/changelog) | | `client.deleteAgent(orgDid, agentDid)` | Tears down an org-owned agent: sweeps every org-data row it holds and deletes its registry record. Its API keys stop authenticating immediately (the delete removes the key's principal). Needs `tee:organisation/contracts` ≥ 0.20.0. | [Register an Organization-owned Agent](/developers/agents/provision-org-agent), [Changelog](/developers/adk/changelog) | | `client.updateAgent(orgDid, agentDid, { agentUri?, ownerDid? })` | Admin-side edit of an agent's registry record. At least one of `agentUri` / `ownerDid` is required. Needs `tee:organisation/contracts` ≥ 0.20.0. | [Register an Organization-owned Agent](/developers/agents/provision-org-agent), [Changelog](/developers/adk/changelog) | | `client.deregisterAgent(agentDid?)` | An agent removing its own registry footprint (registry record + any hosted cards). Omit `agentDid` to act on the caller's own. Needs `tee:agent-registry/contracts` ≥ 1.7.0. | [Register a Public Agent](/developers/agents/register-agent), [Changelog](/developers/adk/changelog) | | `orgData.getDelegation(...)` | Reads back the delegation policy for an org/contract pair. The reply's `grantee_kinds` classifies each grantee as `"agent"`, `"human"`, or `"org"`. | [Member Delegation](/developers/adk/get-started/member-delegation) | | `orgData.countAgentsByContract(input)` / `countAllAgentsByContract(input)` | Counts agents delegated per contract for an org — one page, or drained to exhaustion (throws rather than silently stopping early). Needs org-data ≥ 2.11.0. | [Changelog](/developers/adk/changelog) | | `tenant.setDefaultQuotas(opts)` | Sets an organisation's default per-agent quotas (applied to agents that don't have an explicit override). | [Changelog](/developers/adk/changelog) | | `tenant.updateQuotas(opts)` | Updates quotas for a specific agent, overriding the organisation's defaults. | [Changelog](/developers/adk/changelog) | | `createUsageReportClientFromSession(t3n, baseUrl).getOrgUsageReport(input)` | Fetches the signed per-organisation usage report (`orgDid`, `fromMs`, `toMs`). Caller must admin `orgDid`, or the contract refuses with `not_org_admin: ...`. | [Changelog](/developers/adk/changelog) | | `.getOrgWorkflowExecutionsForWindow(orgDid, fromMs, toMs)` | Counts protected-workflow executions (an agent's calls to `z:`/`y:` client contracts, any outcome) over a window of whole UTC days. Throws if the window is wider than the node will scan in one call, rather than returning a silently-partial count. | [Changelog](/developers/adk/changelog) | | `.getOrgActivityCountForWindow(orgDid, fromMs, toMs)` | Counts every call the organisation's agents made over the window — every contract, every outcome (not just protected workflows). Same window/refusal/truncation rules as `getOrgWorkflowExecutionsForWindow`. | [Changelog](/developers/adk/changelog) | ## Naming conventions | Pattern | Meaning | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `z::` | Tenant-owned KV map or contract, namespaced under your tenant ID. Never include `z::` yourself — pass only the local `tail`. | | `z::public:` | Public (world-readable) tenant map. Never put PII here. | | `{{profile.}}` | Placeholder marker resolved server-side inside the enclave — the literal string is what your contract sends; the real value never enters WASM memory. | | `did:t3n:<40 hex>` | An opaque, platform-assigned tenant or agent DID. Always read it back from the authenticated session — never construct or derive it. | ## WIT host interfaces (contract-side capabilities) Capabilities are determined entirely by which of these you import in `world.wit` — there's no separate manifest. See [Capabilities come from your WIT imports](/developers/adk/tips/capabilities-from-wit-import). | Interface | Purpose | Status | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `http` | Synchronous outbound HTTP. No PII allowed inline. | Available | | `http-with-placeholders` | Outbound HTTP with server-resolved PII substitution. | Available | | `kv-store` | Read/write tenant KV maps. | Available | | `tenant` (`tenant-context`) | Tenant context, e.g. `tenant_did()`. Returns raw bytes (`list`) — hex-encode before use in a `z::` path. | Available | | `logging` | In-enclave logging. | Available | | `outbox` | Enqueue an HTTP request delivered after the transaction commits, at-most-once. | Available | | `ap2` (`ap2/mandate`) | Build/verify AP2 payment mandates. | **Removed** (`testnet-v1.0.10`) — the host interface and its linker world were deleted from the runtime. A contract that still imports it will fail to instantiate. No AP2 replacement has shipped as of this release. | | `did-registry`, `agent-auth`, `user-profile`, `user-removal`, `contracts-call`, `authorisation`, `otp`, `config/read`, `provider-config`, `time/clock`, `node-config`, `stash`, `agent-registry` | Various — see [Host API](/t3n/how-t3n-works/host-api) for the full table. | Confirmed to exist — verify current maturity before relying on one, several are still evolving | | `signing`, `vp` | Cryptographic signing / verifiable-presentation helpers. | Listed as **coming soon** as of the last docs review | ## REST API The full REST surface is documented via OpenAPI — see the [API Reference](/api-reference) tab for live, browsable endpoints generated directly from the spec (so it can't drift out of sync with this page). Verified directly by parsing `terminal-3-openapi.yml` (21 paths, 24 operations, OpenAPI 3.0.3): | Tag | Endpoints | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DID V1 | `GET /v1/did`, `POST /v1/did/register` | | OpenID Connect V1 | `GET /v1/openidc/authorize`, `GET /v1/openidc/credentials`, `GET /v1/openidc/user`, `GET /v1/openidc/user/social_connections`, `POST /v1/openidc/credentials/proof`, `POST /v1/openidc/token` | | OpenID Connect V2 | `GET /v2/openidc/user`, `POST /v2/openidc/token` | | Sub Client V1 | `GET /v1/sub_client`, `GET /v1/sub_client/{subclient_id}`, `POST /v1/sub_client`, `PUT /v1/sub_client/{subclient_id}`, `DELETE /v1/sub_client/{subclient_id}` | | Transactional Email Template V1 | `POST /v1/transactional_email_template`, `POST /v1/transactional_email_template/send`, `PUT /v1/transactional_email_template/:transactional_email_template_id` | | User V1 | `POST /v1/user/create`, `GET /v1/user/{user_id}/social_data`, `GET /v1/user/{user_id}/wallet_addresses` | | VC V1 | `GET /v1/vc/issuer/credentials`, `POST /v1/vc/issuer/credentials/proof`, `POST /v1/vc/issuer/store` | Auth: `bearerAuth` by default, with an `x-api-token` header required on specific endpoints (e.g. user creation). Server: `https://staging.terminal3.io`. ## Reported in community code — checked against the confirmed release (`testnet-v1.0.11`, `@terminal3/t3n-sdk` 5.19.0) These symbols surfaced in community code but weren't in the docs. Here's how each actually stands in the current SDK's public API. (`getAuditEvents()`/`getActivityLog()` used to sit in this section too — moved up to the main table above now that they're confirmed and written up.) | Symbol | Status | | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `buildDelegationCredential()`, `canonicaliseCredential()`, `signCredential()`, `buildInvocationPreimage()`, `signAgentInvocation()` | **Not in the SDK.** These belonged to the per-call *delegation-credential* (envelope) flow, which envelope-free authorisation removed. Use `member-delegation-update` / the SDK's `updateMemberDelegation` instead — see [Member Delegation](/developers/adk/get-started/member-delegation). | | `DelegationCustodialClient` | **Not in the SDK** — no such export at this release. | # AI Coding Assistants & the T3N ADK Skill File Source: https://docs.terminal3.io/developers/adk/support/ai-coding-assistants A ready-to-use skill file — plus this site's machine-readable llms.txt index — that teaches Claude Code, Codex, Cursor, and Windsurf to build confidential AI agents with the T3N ADK. **T3N ADK** is Terminal 3's SDK for building AI agents that can prove who they are, act only on scoped, revocable permission a user actually granted — never a standing API key — and take real-world actions like payments or payroll, all without exposing sensitive information to the model, the application, or the agent runtime (see [What is T3N ADK?](/developers/adk/overview/what-is-adk) and [Why T3N ADK?](/developers/adk/overview/why-adk) for the full picture). If you're building your integration with an AI coding assistant, you don't need to paste this whole documentation site into your context window. Drop the skill file below into your project and the assistant will follow the same order this documentation does: scaffold the project correctly, get a basic authenticated connection working before touching contract code, and check a known-pitfalls table before guessing at a fix. ## Install it Save the file below as `.claude/skills/t3n-adk-quickstart/SKILL.md` in your project (or your global `~/.claude/skills/` directory), then invoke it with `/t3n-adk-quickstart` or just describe what you're building — Claude Code will pick it up automatically based on the skill's description. Save everything below the frontmatter (the `---` block at the top) as `AGENTS.md` in your project root — Codex CLI and Codex cloud agents read this automatically at the start of a session. Save everything below the frontmatter (the `---` block at the top) as `.cursorrules` in your project root. Paste the body of the file (below the frontmatter) as a system-context or rules file, using whatever mechanism your tool supports for persistent project instructions. ```markdown theme={null} --- 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="" \`\`\` ## 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::` 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.* ``` ## Self-service agent credit funding `T3nClient`'s `createAgent` method (via its `initialCredits` option) and its `fundAgent()` method (confirmed live on testnet, `@terminal3/t3n-sdk` 5.16.0+ — see the [Changelog](/developers/adk/changelog)) let a developer fund an agent's T3N credit balance without asking a cluster operator (Terminal 3's own ops team). Both are called on the authenticated client (`t3n.createAgent(...)`, `t3n.fundAgent(...)`) from Step 3 above, not imported as standalone functions. The authorization model behind them is easy to get wrong on a first read, and most of the confusion isn't in the API shape — it's in *why* a call gets refused. The two prompts below encode that model as explicit rules so your assistant can reason about a refusal correctly the first time, rather than guessing from the error text alone. Same install mechanism as the skill file above — save the Claude-flavored one as a Claude Code skill (or append it to the skill above), and the OpenAI-flavored one as a system prompt / project instructions file for an OpenAI-family assistant. ```markdown theme={null} --- name: t3n-agent-credit-funding description: Help a developer fund a Terminal 3 (T3N) agent's credit balance via @terminal3/t3n-sdk's createAgent initialCredits / fundAgent — use when they mention funding an agent, InsufficientCreditError, or agent-credit-transfer. --- # T3N self-service agent credit funding You help a developer integrate self-service Agent DID credit funding through `@terminal3/t3n-sdk` (CLI: `t3n`, npm package `@terminal3/t3n-sdk`). A newly created agent DID starts at **zero credit**; every metered action it takes bills its own balance, so an unfunded agent's first real action always fails. `createAgent` and `fundAgent` are methods on the authenticated `T3nClient` instance (the `t3n` object from the connect-and-authenticate step) — there is no standalone exported function by either name. Always call them as `t3n.createAgent(...)` / `t3n.fundAgent(...)`. ## Funding modes Two ways to get credit onto an agent, both landing in the same underlying contract call (`tee:admin.agent-credit-transfer`): | When | How | SDK | CLI | |---|---|---|---| | At creation | Fund in the same transaction, from the **organisation's own balance** | `t3n.createAgent(org, name, { initialCredits })` | `t3n agent create --org --name --initial-credits ` | | Any time after | Fund from **your own balance** by default, or `fromOrg`'s balance | `t3n.fundAgent(agentDid, amount, { note?, fromOrg? })` | `t3n agent fund --agent --amount [--note ] [--from-org ]` | ## Authorization rules Apply these as strict conditionals, not general guidance. The check lives entirely inside `tee:admin`, re-verifies independently of the caller, and never trusts `tee:organisation` or whatever the SDK/CLI already checked — so there is no way to bypass it by calling something "more directly." - IF funding with no `fromOrg` (self-funding): THEN the caller must administer **at least one organisation the recipient agent belongs to**. It is NOT "fund anyone" — the recipient must be a member of an org you admin. **State this precondition proactively** before suggesting the plain `fundAgent`/`t3n agent fund` command, not after the developer hits a refusal. - IF funding WITH `fromOrg` (org-funding): THEN the caller must administer `fromOrg`, AND the recipient must actually be a member of `fromOrg`. An organisation can only ever fund its own members. - The recipient does not have to be an "agent" specifically — a human member of the same org is an equally valid recipient. This is deliberate, not a gap. - Refusals are always identity-based first, balance-based second: a caller with no standing is refused before their balance (or the source balance) is even checked. - Self-referential funding (`agentDid == caller`) is always refused, on either route. ## Known limitation — state proactively, don't wait for the error There is currently **no self-service way to fund an organisation's own balance.** A freshly created organisation starts at zero. This means `--initial-credits` / `{ initialCredits }` will fail with an insufficient-credit error on a brand-new org — **that is correct, expected behaviour, not a bug.** Only a cluster operator (Terminal 3's own ops team) can top up an organisation's balance today. If a developer is setting up a brand-new org and agent together, tell them this up front before they hit the error. ## Error → diagnosis table Match on these substrings before reasoning from first principles: | Error substring | Diagnosis | |---|---| | `caller does not administer any organisation belongs to` | Self-funding refused — recipient isn't in any org you admin | | `caller is not an admin of organisation ` | Org-funding refused — you don't admin `fromOrg`. **Not a bug** if you're testing authorization; otherwise check org membership | | ` is not a member of organisation ` | Org-funding refused — recipient isn't a member of `fromOrg` | | `insufficient credit (available=0, requested=N)` on an `--initial-credits` / `initialCredits` call | The **organisation's own balance is zero** — see Known limitation above. Not broken. | | `insufficient credit (available=0, requested=N)` on a plain `fundAgent`/`agent fund` call | The **source** balance (your own, or `fromOrg`'s) is zero | | `cannot fund yourself` | `agentDid == caller` — always refused | | `t3n agent fund requires --agent ` / `requires --amount ` | Missing/invalid required CLI flag — straightforward usage fix | ## Common mistakes - **`--json` output is flat.** It's `JSON.stringify(result.data)`, not wrapped in `{ data: ... }`. Use `jq -r .agentDid`, never `jq -r .data.agentDid`. - **Bare `npx @terminal3/t3n-sdk ...` can resolve to a stale cached version** on some hosts, silently missing newer subcommands (`fund`) or fields. Recommend `npx --yes @terminal3/t3n-sdk@latest ...` on every invocation, or install once globally and use the `t3n` binary directly. - **An agent's `apiKey` is returned exactly once, at creation.** There is no way to retrieve it again. Always tell the developer to capture it immediately — never suggest logging or committing it. - **`--org` is mandatory; there is no ownerless funding.** A developer cannot fund "an agent" in the abstract without going through an organisation the caller admins — if they don't have one yet, a one-person org (`t3n org create`) is the practical equivalent of a personal space. - **On the SDK side, these are methods, not imports.** Never suggest `import { createAgent } from "@terminal3/t3n-sdk"` — call `t3n.createAgent(...)` / `t3n.fundAgent(...)` on the already-authenticated client. ## Worked examples Success, at creation: \`\`\`bash t3n agent create --org "$ORG_DID" --name "Booking Bot" --initial-credits 100 # agent created: did:t3n:1a2b3c... # funded with 100 credits from did:t3n:0a1b2c... \`\`\` Success, after the fact: \`\`\`bash t3n agent fund --agent "$AGENT_DID" --amount 50 --note "top-up" # funded did:t3n:1a2b3c... with 50 credits from your own balance (tx seq_no: 42) \`\`\` Refusal — org-funding, caller not an admin: \`\`\`bash t3n agent fund --agent "$AGENT_DID" --amount 50 --from-org "$OTHER_ORG_DID" # error: agent-credit-transfer: caller is not an admin of organisation did:t3n:9f8e7d... \`\`\` This is the authorization check working correctly — it is **not** a bug, and re-running with a different amount or a "retry" will not fix it. The caller needs to actually be an admin of that organisation, or drop `--from-org` to self-fund instead. ## When a developer pastes an error Look it up in the error table above **before** reasoning from first principles. If the error is an insufficient-credit refusal on an `--initial-credits`/`initialCredits` call specifically, check the Known limitation section first — that is almost always the org's own zero balance, not a bug in the developer's code or a platform failure. ``` ```markdown theme={null} You are a coding assistant helping a developer integrate **self-service Agent DID credit funding** for Terminal 3 (T3N), using the npm package `@terminal3/t3n-sdk` and its `t3n` CLI binary. Treat this tool/library identity as fixed context for this conversation: every solution you propose should go through `@terminal3/t3n-sdk`'s `createAgent`/`fundAgent` or the `t3n agent create`/`t3n agent fund` CLI verbs — do not invent alternative APIs. On the SDK side, both are **methods on the authenticated `T3nClient` instance** (`t3n.createAgent(...)`, `t3n.fundAgent(...)`) — never a standalone import. ## Rules (apply first, before any narrative reasoning) 1. A new agent DID starts at **zero credit**. Every metered call bills the acting DID's own balance, so an unfunded agent's first real action always fails with `InsufficientCreditError`-style errors. 2. Two funding paths, same underlying contract call (`tee:admin.agent-credit-transfer`): - `t3n.createAgent(org, name, { initialCredits: n })` / `t3n agent create --org --name --initial-credits ` — funds at creation, from the **organisation's** balance. - `t3n.fundAgent(agentDid, amount, { note?, fromOrg? })` / `t3n agent fund --agent --amount [--note ] [--from-org ]` — funds any time after, from the **caller's own** balance by default, or `fromOrg`'s balance if passed. 3. Authorization (`tee:admin` re-verifies independently every time — never bypassable): - No `fromOrg` → caller MUST administer at least one org the recipient agent belongs to. **State this proactively** before suggesting the plain `fundAgent`/`t3n agent fund` command — don't wait for the refusal. - `fromOrg` set → caller MUST administer `fromOrg` AND the recipient MUST be a member of `fromOrg`. - Recipient may be a human org member, not just an agent — this is intentional. - Identity checks run BEFORE balance checks. - `agentDid == caller` is always refused. 4. KNOWN LIMITATION — state this proactively: there is no self-service way to fund an organisation's OWN balance; only a cluster operator (Terminal 3's own ops team) can. A brand-new org has zero balance, so `--initial-credits` on it will fail with an insufficient-credit error. This is expected, not a bug. ## Error table (match substring first, reason second) | Error substring | Meaning | |---|---| | `caller does not administer any organisation belongs to` | Self-funding refused, recipient not in an org you admin | | `caller is not an admin of organisation ` | Org-funding refused, you don't admin `fromOrg` — correct behavior if testing auth | | ` is not a member of organisation ` | Org-funding refused, recipient not a member of `fromOrg` | | `insufficient credit (available=0, requested=N)` + `--initial-credits`/`initialCredits` | Org's own balance is zero — see Rule 4, not a bug | | `insufficient credit (available=0, requested=N)` + plain fund call | Source (caller's or `fromOrg`'s) balance is zero | | `cannot fund yourself` | `agentDid == caller` | | `t3n agent fund requires --agent ` / `requires --amount ` | Missing/invalid CLI flag | ## Function/CLI option shapes \`\`\`json { "createAgentOptions": { "initialCredits": { "type": "number", "optional": true, "description": "Pre-fund in base units from the org's own balance, same transaction as creation." } }, "fundAgentOptions": { "note": { "type": "string", "optional": true, "description": "Recorded on the ledger tx log — never put secrets or PII here." }, "fromOrg": { "type": "string", "optional": true, "description": "DID of the organisation to fund from instead of the caller's own balance. Requires caller to admin fromOrg and recipient to be a member of it." } } } \`\`\` Validate a developer's intended call against this shape before suggesting code — e.g. `fromOrg` must be a `did:t3n:` string, not a name. ## Common mistakes - `--json` output is FLAT (`jq -r .agentDid`, never `jq -r .data.agentDid`). - Bare `npx @terminal3/t3n-sdk ...` can resolve a stale cached version and silently miss the `fund` subcommand — recommend `npx --yes @terminal3/t3n-sdk@latest ...` or a global install. - `apiKey` is returned exactly ONCE at agent creation, never retrievable again — tell the developer to capture it immediately, never suggest logging/committing it. - `--org` is mandatory. There is no ownerless funding path; a one-person org is the practical equivalent of a personal space for a solo developer. - On the SDK side these are instance methods, not standalone functions — never propose `import { createAgent } from "@terminal3/t3n-sdk"`. ## Examples Success (creation): `t3n agent create --org "$ORG_DID" --name "Bot" --initial-credits 100` → `funded with 100 credits from did:t3n:0a1b2c...` Refusal (org-funding, not an admin): `t3n agent fund --agent "$AGENT_DID" --amount 50 --from-org "$OTHER_ORG"` → `error: agent-credit-transfer: caller is not an admin of organisation did:t3n:9f8e7d...` — this is the authorization check working as designed, not a bug; do not suggest retrying with the same org. ## Final instruction When the developer pastes an error, match it against the error table above BEFORE reasoning from first principles. If it is an insufficient-credit error on an `--initial-credits`/`initialCredits` call specifically, check Rule 4 (org balance) first — this is almost always the cause, not a platform failure or a code bug. ``` ## FAQ A single Markdown file (Claude Code Skill frontmatter + instructions) that teaches an AI coding assistant the exact order to scaffold, connect, and debug a T3N ADK integration — including the known-pitfalls table above. It's not documentation to read; it's meant to be dropped straight into a project so the assistant follows it automatically. See [Install it](#install-it) above for the exact file to save per tool. Claude Code natively (as a Skill), Codex (saved as `AGENTS.md`), Cursor (saved as `.cursorrules`), and Windsurf or any other tool that accepts a plain system-context/rules file — see the tabs under [Install it](#install-it). No. The skill file only teaches the assistant how to call the SDK correctly — it doesn't change what the assistant can see at runtime. Real user data (PII, credentials) is substituted inside the TEE via placeholder markers, so a contract, and anything inspecting or logging it, only ever sees the placeholder, never the plaintext value — it never ends up in the model's context window, a chat log, or your own application logs either. See [Placeholders in outbound calls](/developers/adk/tips/placeholders-outbound-calls) and the [ADK Tour](/developers/adk/overview/adk-tour). Yes — an agent only gets whatever specific functions and external hosts a user explicitly delegates to it; there's no broader standing access to fall back on. See [Member Delegation](/developers/adk/get-started/member-delegation) for how a user grants that scope, and [Outbound HTTP calls are authorized by the user, not the contract](/developers/adk/tips/outbound-http-auth-by-user) for exactly what an agent can and can't reach as a result. It's contained by the same delegation model: an agent never has standing access to anything, so a compromised or misbehaving agent can still only reach what was explicitly delegated to it — no grant, no access. The contract still runs, but any call outside what was actually authorized is simply refused (`host/http.egress_denied`), not silently allowed — see [Outbound HTTP calls are authorized by the user, not the contract](/developers/adk/tips/outbound-http-auth-by-user) for what that refusal looks like in practice. Yes — see [This documentation is agent-readable too](#this-documentation-is-agent-readable-too) below. The skill file is the fast path for building; the raw Markdown pages and `/llms.txt` index are there for open-ended exploration or questions the skill file doesn't cover. ## This documentation is agent-readable too Every page on this site is available as plain Markdown by appending `.md` to its URL, and a machine-readable index of every page lives at [`/llms.txt`](/llms.txt) — point an agent there if you want it to explore the docs directly instead of relying on the skill file above. If you're pasting context manually instead of using the skill file, this single page is the highest-value one to include — every confirmed method, host interface, and API tag in one place. # Dev Community Support Source: https://docs.terminal3.io/developers/adk/support/t3-builder-tg This section provides the various channels for developer community support from Terminal 3. We are dedicated to supporting developers building on the T3 Network. We have a few channels dedicated for developers to ask questions, share ideas, and get help. # Official Developer Telegram Group You can join in the conversation and interact with other developers building on the T3 Network via [terminal3developer](https://t.me/terminal3developer) We will never DM you personally for payment or ask for any sensitive information. Please report any suspicious attempts via the group and we will advise accordingly. # Official Developer Email In the case where more details need to be provided or if you prefer to send an email, you can reach out to us at [devrel@terminal3.io](mailto:devrel@terminal3.io). # Capabilities come from your WIT imports Source: https://docs.terminal3.io/developers/adk/tips/capabilities-from-wit-import Capabilities are determined by the host interfaces imported in your contract's world.wit You don't declare capabilities in a manifest — there isn't one. What your TEE contract can do is decided in two places, both enforced inside the TEE at call time. Your contract runs in one of the `tenant-*` linker worlds, chosen from the host interfaces it imports in `world.wit`. Import `http` and your contract is linked against the `tenant-http` world; import nothing beyond the base and you get `tenant-base` (`kv-store`, `logging`, `tenant-context`). ```wit theme={null} world your-contract { import host:tenant/tenant-context@1.0.0; import host:interfaces/logging@2.1.0; import host:interfaces/kv-store@2.1.0; import host:interfaces/http@2.1.0; // ← opting into outbound HTTP } ``` On top of that, the TEE runtime enforces a capability ceiling — privileged interfaces (signing, user profile, …) are never linked into tenant worlds. See [Host API → z-namespace](/t3n/how-t3n-works/host-api) for the full list. # Common errors Source: https://docs.terminal3.io/developers/adk/tips/common-errors Errors come back as a JSON-RPC **`bad_request`** (HTTP 400) with `{ code: "bad_request", detail, request_id }`. The SDK throws with `detail` — a human-readable message string, **not** a typed error object. Match on the substring shown below. User-authentication failures additionally carry a **machine code at the front** of `detail` (e.g. `eth_authenticator_limit: …`), so the SDK can branch with a single `startsWith`. **Since `testnet-v1.0.10`, every 4xx/5xx response is JSON, not just contract-level `bad_request`s.** Previously, a request rejected before it reached a contract (a malformed query string, an undecodable path segment, an oversized body) could come back as raw `text/plain` instead of the standard envelope. An `error_envelope` middleware now re-encodes any such rejection into the same `{ error, code, request_id }` shape, under the original HTTP status. If your integration special-cased plain-text error bodies, that code path is now dead — everything is JSON. ## Tenant operations — register, maps, dispatch | You'll see in `detail` | Cause | Fix | | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `version is not higher than current version ` | Re-registering a contract at a version that isn't greater than the deployed one | Bump the `version` passed to `contracts.register` | | `map already exists` | Re-running `maps.create` against an already-provisioned tenant | Idempotent — safe to ignore on re-runs | | `map not found` | A map tail in `kv_store::get` / `put` doesn't match what `maps.create` created | Match the tails exactly between [Create tenant KV maps](/developers/adk/tips/create-kv-maps) and your Rust | | `canonical map name invalid: ` | `tail` is empty, contains `..`, or starts with `z:` | Pass only the local tail (e.g. `"secrets"`) — the SDK prefixes `z::` | | `quota exceeded: ` (e.g. `quota exceeded: max_contracts`) | Hit a per-tenant quota | Ask the cluster operator to raise the quota | | `access denied: cannot map ""` | The contract isn't on the map's `readers` / `writers` ACL | `tenant.maps.update` to add the contract id to `readers` / `writers` | | `tenant is suspended` | The operator suspended your tenant | Ask the operator to resume | | `egress host(s) [] not in the resolved allowlist [] (exact string match, so scheme and port must agree)` | The contract called a host the caller's `member_delegation` grant doesn't authorize. Since `testnet-v1.0.10` the message names **both** the rejected host(s) and the actual resolved allowlist, so a scheme/port mismatch (`host:port` requested vs. `host` granted) is visible directly in the error instead of requiring a guess. | Add the exact host (matching scheme/port) to the user's grant (see [Invoke your contract](/developers/adk/get-started/walkthrough/invoke-contract)) | | `no egress allowlist resolved for this call — no matching delegation grant for pii_did='' …` | No delegation grant matched at all — usually a self-call needing a self-grant, or the wrong `pii_did` | Check the grant is for the right `pii_did` and actually covers this call | | `InsufficientCreditError` on a metered function call from an **agent** identity | Metered calls are charged against the **calling identity's own** T3N credit balance — an agent DID's balance is separate from its tenant's, and starts at zero even when the tenant has plenty of test tokens. | For a self-registered agent, get it its own key from the [claim page](/developers/adk/get-started/prerequisites/request-test-tokens) — see [Register a Public Agent](/developers/agents/register-agent). For an **org-owned** agent, fund it directly: `createAgent(..., { initialCredits })` at creation, or `fundAgent(agentDid, amount)` any time after — see [Register an Organization-owned Agent](/developers/agents/provision-org-agent). | Contract-authored errors are whatever **your** contract returns. The flight example, for instance, surfaces `duffel_api_key not found in z::secrets — populate it via the tenant SDK` when the `secrets` map wasn't seeded (see [Seed API key into secrets map](/developers/adk/tips/seed-api-key)) — that's the contract's own message, not a platform error. ## Agent credit funding (`createAgent`'s `initialCredits`, `fundAgent`) Both dispatch into `tee:admin.agent-credit-transfer`, which re-verifies authorisation independently of the caller — it never trusts `tee:organisation` or the SDK/CLI that invoked it. | Error (verbatim substring) | Cause | What it means | | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `agent-credit-transfer: caller does not administer any organisation belongs to` | Self-funding (no `fromOrg`) refused — the recipient isn't in any organisation you administer | Fund an agent under an org you actually admin, or pass `fromOrg` explicitly | | `agent-credit-transfer: caller is not an admin of organisation ` | Org-funding (`fromOrg` set) refused — you don't administer `fromOrg` | Expected/correct if you're testing authorisation; otherwise check org membership | | `agent-credit-transfer: is not a member of organisation ` | Org-funding refused — the recipient isn't a member of `fromOrg` | An organisation can only fund its own members | | `agent-credit-transfer: insufficient credit (available=0, requested=N)` | The source balance (personal or org) is actually zero | **A brand-new organisation always starts at zero balance.** If this hit on `createAgent(..., { initialCredits })`, that's expected — there is currently no self-service way to fund an organisation's own balance; only a cluster operator can. This is not a bug. | | `agent-credit-transfer: cannot fund yourself` | `agentDid` equals the caller's own DID | Self-referential funding is always refused, self-funding or org-funding alike | | `t3n agent fund requires --agent ` / `t3n agent fund requires --amount ` | Missing/invalid required CLI flag | Straightforward CLI usage fix | ## Generic / opaque failures (HTTP 500, no clear cause) A `bad_request` gives you a substring to match on. A bare **`HTTP 500`** with no further detail is a different situation — it usually isn't your contract logic. Before assuming it's a bug in your code, check these in order: 1. **Grab the `request_id`.** Most 500 responses still include one — save it before you start changing code. If you end up asking for help, it's the single most useful thing you can hand over. 2. **Re-check egress and ACLs first.** A missing outbound-host grant or a missing map ACL entry can surface as a 500 instead of the more specific error you'd expect — re-read the [egress](/developers/adk/tips/outbound-http-auth-by-user) and [map ACL](/developers/adk/tips/create-kv-maps) requirements before looking further. 3. **Retry once, deliberately.** A single unhealthy node can return a 500 for requests that are otherwise correct. If an identical request succeeds on retry, it was very likely transient — no code change needed. 4. **If it's consistent and reproducible**, it's more likely a platform-side issue than your integration. Report it in the [developer Telegram](https://t.me/terminal3developer) with the `request_id`, rather than spending hours guessing at workarounds. ## Common integration gotchas These aren't errors with a fixed message — they're patterns that produce confusing behavior downstream, reported independently by multiple teams. | Gotcha | What happens | Fix | | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | Using `tenant_did()`'s return value directly in a map path | `tenant_did()` returns raw bytes (`list`), not a string. Using it directly (or formatting it with `{}`) is wrong and won't compile against a `String` map path — it must be hex-encoded first. | Hex-encode it: `format!("z:{}:secrets", hex::encode(&tenant_did()))` — this matches the map the tenant SDK created. | | Omitting `baseUrl` when constructing a `TenantClient` | `TenantClient`'s config accepts `baseUrl` as an optional field, but calls can still fail at request time without it, even after calling `setEnvironment()`. This is specific to `TenantClient` — `T3nClient` (used for tenant, agent, and user sessions in the walkthrough) doesn't take a `baseUrl` at all; it's always resolved from the active environment. | When constructing a `TenantClient`, always pass `baseUrl: getNodeUrl()` explicitly rather than relying on it being inferred. | ## Authentication & wallet linking These come from the user/session contract during sign-in and `addAuthMethod`, with the code at the **front** of `detail`: | Code (prefix of `detail`) | When | | ------------------------- | --------------------------------------------------------------------------- | | `eth_authenticator_limit` | Hit the cap on wallets linked to one DID (e.g. trying to add an 11th) | | `eth_auth_map_conflict` | The wallet is already linked to a different DID — resolve via account merge | | `email_not_verified` | A profile upsert ran before the email OTP was verified | | `user_not_found` | The DID has no profile yet | | `legacy_field` | A pre-2.0.0 dispatch field was sent (e.g. `otp_code` on `user-upsert`) | # Create Tenant KV Maps Source: https://docs.terminal3.io/developers/adk/tips/create-kv-maps A TEE contract needs one map before it can run: `secrets`, holding the API key. Create it with the `TenantClient`. The `tail` is the per-map local name; the host stores it as `z::`. ```typescript theme={null} await tenant.maps.create({ tail: "secrets", visibility: "private", writers: { only: [contractId] }, readers: { only: [contractId] }, // REQUIRED — the kv-governor denies reads when omitted }); ``` `readers` **must** be set explicitly — the KV governor defaults to **deny**, so leaving it off makes the contract's own secret read fail with `AccessDenied`. `MapAlreadyExists` is idempotent — safe to re-run when re-deploying. **Map visibility quick reference:** * `"private"` — only your contracts can access this map (default, use it for everything sensitive). * `"public"` — world-readable via `/api/dev/public-kv//`. Map tail must start with `public:`. Never put PII in a public map. `writers`/`readers` restrict your **contracts** — including ones you deploy yourself — not you. As the map's owner you can always write its entries directly via the control plane (`tenant.executeControl("map-entry-set", …)`), even on a `writers: { only: [contractId] }` map, or add a contract to `writers` via `tenant.maps.update` if you'd rather it write directly — that's how [seeding the API key](/developers/adk/tips/seed-api-key) works. A contract-only map is **not** tamper-proof against its owner; see [Storage Namespaces → Access model](/t3n/how-t3n-works/z-namespace#access-model). # Audit Trail: Read Your Organization's Activity Log Source: https://docs.terminal3.io/developers/adk/tips/get-activity-log A tamper-evident audit trail of every action an agent took, provable to a compliance team or auditor after the fact — page through it with client.getActivityLog(). `client.getActivityLog(opts?)` returns one page of your **organization's** agent-activity audit trail — every audited contract dispatch (actor, on-behalf-of, contract, function, outcome), reconstructed from the cluster's append-only, tamper-evident ledger. This is what you point a compliance team or an auditor at to prove what an agent actually did, not just what it was supposed to do. The scope organization is resolved server-side from your session DID: there's no scope parameter, and a page carries **every member's** activity in that org, not just your own. ```typescript theme={null} let cursor: number | undefined; do { const page = await client.getActivityLog({ limit: 200, ...(cursor !== undefined && { before_seq: cursor }), }); render(page.entries); cursor = page.next_seq ?? undefined; } while (cursor !== undefined); ``` **Page until `next_seq` is `null` — never stop on a short or empty page.** The node caps how much ledger a single call decrypts, so a page can come back shorter than `limit` (even empty) while the window still has rows and `next_seq` is still non-null. Only a `null` `next_seq` means the window is exhausted. Rows come back **newest first** (descending `seq_no`). Each `ActivityEntry`: | Field | Type | Meaning | | -------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `seq_no` | `number` | Ledger index — the ordering key. Use this, not `timestamp_ms`, to sort: entries from different nodes can have skewed clocks. | | `hash` | `string` | Hex SHA-256 of the stored entry — read-time tamper evidence. | | `timestamp_ms` | `number` | Call time in Unix ms, stamped on the node that took the request (per-node wall clock, not cluster-assigned). | | `caller_type` | `"agent" \| "human"` | Decided server-side by an existence check on `actor` in the agent registry. | | `actor` | `string` | DID that actually invoked the contract. | | `on_behalf_of` | `string` | DID of the owner the caller acted for. Equals `actor` on a self / own-authority call — never null. | | `org` | `string` | Organization the acting principal belongs to — identical on every row of a page. | | `contract` | `string` | Contract that ran, e.g. `tee:user/contracts`. | | `function` | `string` | Contract function invoked, e.g. `user-upsert`. | | `outcome` | `"success" \| "denied" \| "error"` | Host-stamped result of the dispatch. | | `roles?` | `string[]` | Role ids the acting authority carried at call time. Omitted when the authority carried no role. **On a `denied` entry this is the caller's *claimed* authority, not a validated one.** | Params (`GetActivityLogOptions`), all optional — `{}` is a valid call: `from_ms` / `to_ms` bound the window (Unix ms, inclusive); `before_seq` is the paging cursor (the previous page's `next_seq`); `limit` is clamped server-side to 1–1000 (default 200). Like `execute`, this runs the session-payload encryption rather than plain HTTPS — entries carry principal DIDs, so `getActivityLog()` requires an authenticated session. **Not the same method as `getAuditEvents()`.** `getAuditEvents()` returns *your own* audit trail (every actor, PII-bearing action/target/details included) — it's the right call when you need your own history with detail. `getActivityLog()` returns your *organization's* activity, metadata only (no call arguments or payloads), scoped to every member. Reach for `getActivityLog()` for an org-wide activity view; reach for `getAuditEvents()` for your own detailed trail. # Outbound HTTP calls are authorized by the user, not the contract Source: https://docs.terminal3.io/developers/adk/tips/outbound-http-auth-by-user Your TEE contract does not declare which hosts it may call. A tenant contract's outbound HTTP egress is resolved, on every call, from the **calling user's authorization grant** — the *allowed hosts* the user grants when they delegate to your agent or contract: * **Delegated call** → the subject user's grant. * **Direct (self) call** → the caller's own self-grant. If the target host (for example `api.duffel.com`) isn't on the grant's allowed-hosts list, the contract still runs but the outbound call is denied with `host/http.egress_denied`. This is the most common reason a working contract can't reach its API: the code is fine, but no grant authorizes the host. Set the grant before you invoke — see [Invoke your contract](/developers/adk/get-started/walkthrough/invoke-contract) and [Delegate access](/t3n/data-owner-guide/delegate-access). # Placeholders in outbound calls Source: https://docs.terminal3.io/developers/adk/tips/placeholders-outbound-calls Send private data to a third-party API without it ever entering your contract, using http-with-placeholders. When your contract needs to send private data (e.g., PII — name, date of birth, email, etc.) to a third-party API, it does **not** read the values and inline them. Instead it uses the **`http-with-placeholders`** host interface: you put `{{profile.}}` markers in the request, and the host resolves them from the calling user's profile **inside the enclave**, just before the request goes out. The plaintext never enters your WASM. ``` Agent → z::contract → host (http-with-placeholders) → Duffel book-offer templates {{profile.*}} resolves the markers from POST /orders into the order body the calling user's profile, (real PII) then sends the rendered request { id, pnr } ◀─────────────────────────────────────────────────────────────── { id, pnr } ``` Your contract in Rust: ```rust theme={null} use crate::host::interfaces::http_with_placeholders as hwp; // The {{profile.}} markers are resolved host-side from the calling // user's profile — this contract never sees the plaintext values. let body = serde_json::json!({ "data": { "type": "instant", "selected_offers": [req.offer_id], "passengers": [{ "id": "passenger_0", "given_name": "{{profile.first_name}}", "family_name": "{{profile.last_name}}", "born_on": "{{profile.date_of_birth}}", "email": "{{profile.verified_contacts.email.value}}", }] } }); let resp = hwp::call(&hwp::Request { method: "POST".to_string(), url: "https://api.duffel.com/air/orders".to_string(), headers: vec![ ("Authorization".to_string(), format!("Bearer {api_key}")), ("Duffel-Version".to_string(), "v2".to_string()), ("Content-Type".to_string(), "application/json".to_string()), ], payload: Some(serde_json::to_vec(&body)?), })?; // resp.payload — Duffel's response (booking id + PNR). The passport/name/DOB // were substituted by the host; your WASM never held them. ``` Key points: * **Synchronous.** Like plain `http`, you get the upstream response back in the same invocation — there's no deferred queue. * **Profile access is gated by the user's delegation.** The markers resolve only when the calling agent is authorized to act for that user (see [Invoke your contract](/developers/adk/get-started/walkthrough/invoke-contract)). A marker your contract isn't permitted to resolve fails with `placeholder not permitted: `. * **Egress is the same rule as `http`.** The target host must be on the user's allowed-hosts grant, or the call is denied (`host/http.egress_denied`). * **Markers reference the user profile schema** — e.g. `{{profile.first_name}}`, `{{profile.date_of_birth}}`, `{{profile.verified_contacts.email.value}}`. Fields the schema doesn't carry yet (passport, title) are supplied by your contract directly. # Running the code samples in this guide Source: https://docs.terminal3.io/developers/adk/tips/running-code-samples How to actually get a TypeScript snippet from these docs into a runnable file, without it landing in your shell instead. Every code block on this site tells you what to write, not how to get it into a file. This page is that missing step — read it once, then every "create a file and paste this in" or "append this to the bottom of" instruction elsewhere means exactly the pattern below. **To create a new file:** ```bash theme={null} cat > quickstart.ts << 'EOF' // ...the code block from the page... EOF ``` **To append to an existing file** (most snippets past the first one — they assume `t3n`/`tenantDid`/`wasmComponent` are already in scope from earlier in the same file): ```bash theme={null} cat >> quickstart.ts << 'EOF' // ...the code block from the page... EOF ``` **Copy the whole block, including the `cat` line and the closing `EOF` — never just the code in between.** Pasting only the TypeScript directly at your shell prompt doesn't write a file at all; bash tries to *run* each line as a command instead. `import { T3nClient, ... }` becomes an attempt to run a program called `import`, `T3nClient,` becomes an unknown command, and every `{`/`(` produces a `syntax error near unexpected token`. If you see a wall of `command not found` / `syntax error` lines that look nothing like a TypeScript error, this is almost always what happened — nothing was written, so just redo the full block. **The delimiter must be quoted — `<< 'EOF'`, not `<< EOF`.** TypeScript's non-null assertion operator (`process.env.T3N_API_KEY!`) contains a bare `!`, and bash's interactive history expansion treats an unquoted `!` as "repeat a previous command," failing with `-bash: !: event not found`. Quoting the delimiter turns off expansion for everything inside the block. This bites people even *inside* a correctly-shaped `cat << EOF` if the quotes were dropped — always `'EOF'`. If you'd rather not think about quoting every time, disable the feature that causes it, once per shell session: ```bash theme={null} set +H ``` After that, `<< EOF` and `<< 'EOF'` behave identically for the rest of the session, and pasting a bare `!` anywhere no longer triggers `event not found`. This doesn't fix the separate "pasted at the prompt instead of into a heredoc" mistake above — only quoting (or a heredoc at all) does that. Before running anything, it's worth a quick sanity check that the file actually contains what you think it does — especially after a paste that might have gone wrong: ```bash theme={null} tail -20 quickstart.ts ``` Then run it the same way every page in this guide expects: ```bash theme={null} npx tsx quickstart.ts ``` # Seed API key into secrets map Source: https://docs.terminal3.io/developers/adk/tips/seed-api-key Seed the API key into the secrets map using the map-entry-set control call. Your contract reads the API key from `z::secrets` at runtime. There's no `set-credentials` function — the tenant SDK writes the key straight into the map with the `map-entry-set` control call, on the authenticated `tee:tenant/contracts` path (not an agent call). ```typescript theme={null} await tenant.executeControl("map-entry-set", { map_name: tenant.canonicalName("secrets"), key: "duffel_api_key", value: process.env.DUFFEL_API_KEY!, }); console.log("API key sealed in z::secrets — not visible outside the TEE"); ``` What happens: 1. `map-entry-set` writes the value into `z::secrets`. It is a control-plane write, so it **bypasses the map's `writers` ACL** — the key lands even though the map is read/write-restricted to the contract alone (see [Create tenant KV maps](/developers/adk/tips/create-kv-maps)). 2. At call time your contract reads it back with `kv_store::get(&format!("z:{}:secrets", hex::encode(&tenant_did())), b"duffel_api_key")` inside the TDX enclave — `kv-store::get` takes the full canonical map name (not the bare tail) and a byte-string key. The only path to the key is through your contract code — no external observer, not the agent, not the calling developer, can read it back out. # Verify the trust anchor Source: https://docs.terminal3.io/developers/adk/tips/verify-trust-anchor What fetchTrustedManifest actually checks, and when the unsafe_trust_server opt-out is (and isn't) appropriate. `trustAnchor` is a **required** field on `T3nClient` — the client throws immediately at construction if it's missing or malformed. It decides whether the SDK verifies it's really talking to a genuine T3N enclave running the official image, or simply trusts whatever the server presents. There are three valid values, and the difference is a security boundary, not a style preference: | Option | Meaning | Use for | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | `await resolveTrustAnchor(env)` | One-call convenience: fetches and verifies the environment's trust manifest and hands back a ready-to-use `TrustAnchor`. Same verification as `fetchTrustedManifest`, less boilerplate. | Any hosted node (testnet, production) — the default recommendation | | `await fetchTrustedManifest(env)` | Fetches the operator-signed trust manifest for the environment, verifies its signature against a public key baked into the SDK, and returns a verified anchor. Use this form directly when you need the lower-level control (e.g. passing `{ minVersion }`, see below). | Any hosted node (testnet, production) | | `{ unsafe_trust_server: true }` | No verification at all — the client accepts whatever attestation the server presents. | Local dev nodes only | ```typescript theme={null} import { T3nClient, resolveTrustAnchor, loadWasmComponent } from "@terminal3/t3n-sdk"; const client = new T3nClient({ trustAnchor: await resolveTrustAnchor("testnet"), wasmComponent: await loadWasmComponent(), // ...handlers, as in Quickstart }); ``` `unsafe_trust_server` is correct **only** for local development. A local node has no genuine TDX hardware quote to present, so real verification cannot succeed there. Never pass it against a hosted node, and never write a `catch` that silently falls back to it — that would remove the protection without telling you the cluster stopped serving a manifest. What happens: `resolveTrustAnchor(env)` (or the equivalent `fetchTrustedManifest(env)` call) fetches the manifest the node itself serves at `GET /api/trust-manifest`, checks its ECDSA signature against a public key pinned in the SDK, and — only if that passes — returns a `TrustAnchor` pinning the node's expected peer IDs and two allow-listed measurements: `rtmr1_allowlist` and `rtmr3_allowlist`. If the manifest is missing, unsigned, signed by the wrong key, or older than one this process already accepted, it throws rather than falling back to trusting the server. Handle the throw; don't catch it and substitute `unsafe_trust_server`. **RTMR1 is the measurement that matters now.** As of `testnet-v1.0.10`, the node measures rootfs integrity via dm-verity + a Unified Kernel Image, which lands in **RTMR1** — `rtmr1_allowlist` is the real, current integrity signal. `tee-init.sh` no longer extends RTMR3, so `rtmr3_allowlist` is kept only for backward compatibility with older attestation consumers; don't treat it as meaningful going forward. `TrustAnchor` gained `rtmr1_allowlist` as a **required** field in this release — if you construct a `TrustAnchor` by hand instead of through `resolveTrustAnchor`/`fetchTrustedManifest`, you must supply it. **Attestation verification is now vendor-aware.** As of `testnet-v1.0.10`, the SDK can verify both TDX (RTMR1/RTMR3) and AMD SEV-SNP attestation — the trust manifest schema gained two optional fields, `sev_snp_measurement_allowlist` and `sev_snp_product`, alongside the existing `rtmr1_allowlist`/`rtmr3_allowlist`. `resolveTrustAnchor`/`fetchTrustedManifest` handle both transparently — nothing changes in the code above regardless of which hardware the cluster you're targeting runs on. If you need the lower-level primitives directly (verifying a raw quote yourself rather than going through a `T3nClient`), new exports cover the SEV-SNP side: `verifyDkgAttestation` (vendor-aware, dispatches automatically), `verifySevSnpReport`, `detectQuoteVendor`, `sevSnpVcekUrl`, `fetchSevSnpCollateral`. The existing `verifyTdxQuote` is unchanged. Most developers on the shared testnet/production clusters (TDX hardware) will never need these directly — they matter for a private-cloud cluster that runs on SEV-SNP hardware. **Rollback protection is per-process.** The manifest version floor is held in memory and lost when your process restarts. If you need a client to reject an older manifest across restarts (protecting against a replayed pre-rotation manifest), persist the accepted version yourself and pass it back via `fetchTrustedManifest(env, { minVersion })`. A signed manifest rolls out per-environment alongside each cluster's deploy, so an environment can exist in the SDK before its nodes are actually serving one — `fetchTrustedManifest` throws a fetch error in that case, not a signature error. If that happens for an environment you expect to work, the cluster likely hasn't published a manifest yet; it will once it next redeploys. Until then, target an environment that already has one, or opt out explicitly and knowingly with `{ unsafe_trust_server: true }` (local dev only — see the Warning above). # Payroll Agent Source: https://docs.terminal3.io/developers/adk/use-cases/payroll-agent See [Delegate Access to AI Agents](/t3n/use-cases/delegate-access-to-agent#payroll) # Register an Organization-owned Agent Source: https://docs.terminal3.io/developers/agents/provision-org-agent Mint an agent your organization owns and controls, with a private agent card the org manages — from installing the SDK to reading the card back. This guide takes an organization from nothing to owning an agent on T3N: a `did:t3n` DID the organization controls, with an agent card that **T3N hosts privately** for the organization to manage — never public unless the organization chooses. Every step uses the `t3n` CLI that ships with the [`@terminal3/t3n-sdk`](https://www.npmjs.com/package/@terminal3/t3n-sdk) package — no code required, for the whole lifecycle: create, fund, read, update, and delete (updating and deleting need CLI 5.8.0 or newer; older installs have an SDK fallback for update). **Onboarding, not permissions.** This page gives an agent an *identity* and a *card*. It does not grant the agent access to anything — no user data, no contract functions. That's a separate step the data owner performs afterwards: see [Member Delegation](/developers/adk/get-started/member-delegation). **Two onboarding paths.** This is the one where an **organization owns** the agent, with a card kept **private by default**. If instead an agent **registers itself** — public and discoverable — see [Register a Public Agent](/developers/agents/register-agent). The flow is: 1. Download the SDK 2. Get an org admin's key 3. Create your organization 4. Scaffold the agent card 5. Provision the agent (and fund it) 6. Read the card back All commands that talk to the network accept `--env sandbox|testnet|production` (or the `T3N_ENV` environment variable); `sandbox` and `testnet` are the same test network. `testnet` is the default — the examples below pass it explicitly anyway, so a copied command never depends on your shell. The CLI is part of the TypeScript SDK. You can run it with zero install, or install it globally: ```bash theme={null} # zero-install — always runs the latest published version npx @terminal3/t3n-sdk --help # or install globally (Node.js >= 18) pnpm add -g @terminal3/t3n-sdk t3n --help ``` The rest of this guide uses the global `t3n` form; substitute `npx @terminal3/t3n-sdk` if you skipped the install. The organization admin's key is a standard Ethereum-style **secp256k1 private key** (32 bytes, `0x`-prefixed hex). Provisioning is a write operation that consumes credits, so get this key from the [claim page](/developers/adk/get-started/prerequisites/request-test-tokens) — it's self-serve, issues a fresh key together with metered test credits every time you visit, and a key generated any other way starts with zero credits and can't pay for this step. Put the key in your shell's environment; every signing command reads it from there (or from `--api-key`): ```bash theme={null} export T3N_API_KEY="0x" ``` Give the admin its **own** key and keep it out of source control. Every command below is signed by this key, so whoever holds it controls the organization and the agents it owns. Skip this if your organization already exists — just use its DID. Otherwise create one; the authenticated caller becomes its initial admin: ```bash theme={null} export ORG_DID=$(t3n org create --name "Acme Robotics" --env testnet --json | jq -r .organisationDid) echo "$ORG_DID" # did:t3n:0a1b2c... ``` Drop `--json` for human-readable output (`organisation created: did:t3n:0a1b2c...`) if you'd rather copy the DID by hand. `org create` is **not** idempotent — every call mints a *new* organization. Run it once and keep the DID; re-running it because you lost the DID leaves an orphan org behind. **A freshly created organization has zero T3N balance.** There is currently no self-service way to fund an organization's own balance — only a cluster operator can top it up. This matters for the next step: `--initial-credits` on a brand-new org will fail with an insufficient-credit error until the org itself has been funded. That's expected, not a bug — see [Common Errors](/developers/adk/tips/common-errors#agent-credit-funding-createagent-s-initialcredits-fundagent). The agent card is a JSON document, following the [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) registration format, that describes the agent and lists its service endpoints — including its [A2A](https://a2a-protocol.org/) agent card. Scaffold one with: ```bash theme={null} t3n agent create-card --out agent-card.json ``` Run interactively, it walks you through name, description, image, and which services to include (A2A / MCP / DID). In scripts it falls back to flags: `--name`, `--description`, `--image`, `--did`, `--x402`, and `--force`. Replace the placeholders with your agent's real endpoints and keep the whole card under **16 KiB**. This step is **optional** — skip it to let T3N host a minimal default card for the agent instead (shown in the next step). Mint the agent and host its card, all in one transaction. You don't need a URL for the card — T3N serves it at `/api/agent-card/`: ```bash theme={null} t3n agent create \ --org "$ORG_DID" \ --name "Booking Bot" \ --card agent-card.json \ --initial-credits 100 \ --env testnet # agent created: did:t3n:1a2b3c... # ⚠ API key (shown ONCE — store it now, it is the agent's credential): # t3n_key_43c008200b3a6c2c.6aab9ce6be8f33ba... # key id: 43c008200b3a6c2c # private card hosted from agent-card.json (publish with: t3n agent card-publish ...) # funded with 100 credits from did:t3n:0a1b2c... ``` One transaction mints the agent's DID and its wallet, hosts the card **privately** under the organization, and — with `--initial-credits` — funds the agent from the **organization's own balance** in that same transaction. Nothing is published — the agent is not publicly discoverable, by design. **`--initial-credits` is optional but strongly recommended.** Every metered action an agent takes is billed to the *agent's own* T3N balance, which is separate from the organization's. Skip `--initial-credits` and the newly created agent starts at **zero** credit — its very first real action will fail with `InsufficientCreditError` until you fund it. If you do skip it, fund the agent afterwards with `t3n agent fund`: ```bash theme={null} t3n agent fund --agent "$AGENT_DID" --amount 100 --note "initial top-up" # funded did:t3n:1a2b3c... with 100 credits from your own balance (tx seq_no: 42) ``` `agent fund` defaults to funding from **your own** (the caller's) balance rather than the organization's — pass `--from-org "$ORG_DID"` to fund from the organization's balance instead, same as `--initial-credits` does at creation time. Either way you must administer an organization the agent belongs to; the check is enforced independently on the node, not merely by the CLI. Funding isn't a one-time thing either — run `agent fund` again any time the agent's balance runs low. See the [SDK & API Reference](/developers/adk/reference) for the equivalent `fundAgent()` SDK call and the [Common Errors](/developers/adk/tips/common-errors#agent-credit-funding-createagent-s-initialcredits-fundagent) table for what each refusal means. **The API key is printed exactly once and cannot be recovered.** Store it now — it is the agent's *own* credential, what the agent uses to authenticate as itself. The agent's secp256k1 key is minted **inside the TEE and never leaves it**; you never see a private key. What you get instead is an opaque bearer token, `t3n_key_.`, of which only a hash is stored on-ledger — which is why it is unrecoverable rather than merely inconvenient to look up. The agent presents it verbatim in the `X-T3N-Api-Key` header of a [stateless `POST /api/invoke`](/developers/adk/get-started/member-delegation#3-stateless-invocation-with-invoke) call. Scripting? Use `--json` and capture `.apiKey`, plus `.keyId` (the 16-hex lookup id, safe to log and to store alongside your records — the secret half is not). **Drop `--card` to host a default card.** `t3n agent create --org "$ORG_DID" --name "Booking Bot"` (nothing else) hosts a minimal default card, named after the agent, with its DID as the service entry: ```json theme={null} { "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1", "name": "Booking Bot", "description": "T3N-registered agent (default card)", "services": [ { "name": "DID", "endpoint": "did:t3n:1a2b3c...", "version": "v1" } ], "x402Support": false, "active": true, "registrations": [], "supportedTrust": ["tee-attestation"] } ``` Pass `--no-card` to mint the agent with **no** card at all — add one later with `card-set`, which needs the writer grant covered in the last step. And pass `--uri ` only if the agent also has its **own** external endpoint you want recorded in its DID document (an `agent-uri` service entry is added for it). The card lives in your organization's private scope. Read it back as an org admin — **no publishing required**: ```bash theme={null} export AGENT_DID="did:t3n:1a2b3c..." t3n agent card-get --owner "$ORG_DID" --agent "$AGENT_DID" --env testnet # → the card JSON, returned to authorized readers only ``` Only the organization's admins — or a principal you grant a delegation credential to — can read it. It is **not** exposed at the public `/api/agent-card/` endpoint. That's the whole lifecycle for a private org agent — create, read, update — all authenticated, none of it public. If `card-get` returns your card, you're done; updating it takes the one extra grant below. **Updating the card needs one more grant.** `t3n agent card-set --owner "$ORG_DID" --agent "$AGENT_DID" --file agent-card.json` fails for a freshly created org with: ``` error: RPC Error: NotScopeWriter: signing user is not a writer for this scope ``` This is by design, not a bug: org-data keeps two roles separate — **admins manage policy, writers manage data** — and being an admin does not imply write access to any scope. The card you just read was written by the *contract* during `agent create`, not by you. To write it yourself, add your own DID to the `agent-cards` scope's writer list. One call, once per scope: ```bash theme={null} t3n org writers-add --org "$ORG_DID" --scope agent-cards --writer "$(t3n whoami)" --env testnet # writers updated for scope 'agent-cards' ``` `card-set` then succeeds, and `card-get` returns the updated card. Check the list any time with `t3n org writers-get --org "$ORG_DID" --scope agent-cards`. The writer verbs need `@terminal3/t3n-sdk` **4.25.0 or newer** (`npx @terminal3/t3n-sdk` always runs the latest; a pinned older install won't have them). On an older CLI, grant it from the SDK — but `setWriters` **replaces** the list, so read it first and merge, or you will revoke every other writer on the scope: ```ts theme={null} import { SessionOrgDataClient, AGENT_CARDS_SCOPE } from "@terminal3/t3n-sdk"; const orgData = new SessionOrgDataClient(t3n, nodeUrl); // authenticated as an org admin const { writers } = await orgData.writersGet({ orgDid, scope: AGENT_CARDS_SCOPE }); await orgData.setWriters({ orgDid, scope: AGENT_CARDS_SCOPE, writers: [...new Set([...writers, adminDid])], }); ``` **Granting and revoking write access.** `writers-add` and `writers-remove` adjust the list and leave the other writers alone — the usual choice. `writers-set` **replaces** the whole list, so anyone you omit loses access; `writers-clear` removes everyone, leaving the scope unwritable until you grant again. All four require an org admin. `add` and `remove` read the current list and write back the result, so two admins editing the same scope at the same moment can overwrite each other. That's fine interactively; scripts that might race should use `writers-set` with the full intended list. Reads are metered too. If `card-get` returns `InsufficientCredit`, the account is simply out of credits — top up and retry; nothing is wrong with the card. Metering settles after the fact, so the writes in earlier steps can succeed and then leave the balance short for this read. **Publishing for public discovery (optional).** To list the agent publicly — resolvable by anyone, without a key, at `GET /api/agent-card/` — publish it explicitly: `t3n agent card-publish --owner "$ORG_DID" --agent "$AGENT_DID"`. Consent is already in place (the agent was minted with your organization linked), so it's a single call, reversible with `card-unpublish`. Most org-internal agents never need it. The card is ordinary org-data: `create`/`card-set` store it in your organization's `agent-cards` scope on the built-in `tee:org-data/contracts` TEE contract, at a deterministic entry id derived from `(orgDid, agentDid)`. Reads (`card-get`) go through org-data's normal authorization — an org admin, or a holder of a delegation credential scoped to that read. It is never copied to the world-readable card map unless you explicitly publish it. ## Updating or deleting the agent Two more admin-side operations complete the lifecycle, both requiring an org admin's key (`T3N_API_KEY` from earlier in this guide): **Update the agent's registry record** — its `agentUri` and/or `ownerDid`: ```bash theme={null} t3n agent update --org "$ORG_DID" "$AGENT_DID" --uri "https://agent.example/.well-known/agent-card.json" # agent updated: did:t3n:1a2b3c... ``` Pass `''` (empty string) to clear a field rather than change it; a flag given with no value throws — only omitting the flag entirely leaves that field untouched. At least one of `--uri` / `--owner` is required per call. **Delete the agent** — a full teardown, not a soft-disable: ```bash theme={null} t3n agent delete --org "$ORG_DID" "$AGENT_DID" # Delete agent did:t3n:1a2b3c... from org did:t3n:0a1b2c... on testnet? # Its API keys stop working and cannot be restored. [y/N] ``` **This is destructive and irreversible.** Deleting an agent sweeps every org-data row it holds (its card, its footprint) and deletes its registry record in the same transaction. Its API keys stop authenticating **immediately** — the delete removes the key's underlying principal, not each key individually, so there's no way to enumerate and revoke them separately, and no way to bring the agent back with the same DID. Confirm interactively (`y`/`N`), or pass `--yes` to skip the prompt in a script — do so only once you're certain. Both commands need `tee:organisation/contracts` ≥ 0.20.0 — an older cluster refuses with a clear upgrade message rather than a silent no-op. The equivalent SDK calls are `client.updateAgent(orgDid, agentDid, { agentUri?, ownerDid? })` and `client.deleteAgent(orgDid, agentDid)` — see the [SDK & API Reference](/developers/adk/reference). ## What's next * The agent now has an identity and a card. Next, let it actually *do* something — granting permission to act on users' data and contracts → [Member Delegation](/developers/adk/get-started/member-delegation) and [Delegate Access](/t3n/data-owner-guide/delegate-access) * How DIDs work on T3N → [Decentralized Identifiers](/t3n/how-t3n-works/did) * Provisioning and card writes consume credits → [Tokens](/t3n/how-t3n-works/tokens) * Full method/CLI history for agent create/fund/update/delete/deregister → [Changelog](/developers/adk/changelog) # Register a Public Agent Source: https://docs.terminal3.io/developers/agents/register-agent Give your agent a T3N identity and publish its agent card — from installing the SDK to verifying the registration. This guide takes an AI agent from nothing to a publicly resolvable identity on T3N: a `did:t3n` DID plus an agent card that **T3N hosts and serves** for other agents and services to discover — no external storage required. Every step uses the `t3n` CLI that ships with the [`@terminal3/t3n-sdk`](https://www.npmjs.com/package/@terminal3/t3n-sdk) package — no code required. **Onboarding, not permissions.** This page gives an agent an *identity* and a *card*. It does not grant the agent access to anything — no user data, no contract functions. That's a separate step the data owner performs afterwards: see [Member Delegation](/developers/adk/get-started/member-delegation). **Two onboarding paths.** This is the one where an agent **registers itself** — public and discoverable. If instead an **organization** provisions and owns the agent (with a card kept **private by default**), see [Register an Organization-owned Agent](/developers/agents/provision-org-agent). The flow is: 1. Download the SDK 2. Get the agent's key 3. Get the agent's DID 4. Scaffold the agent card 5. Host the agent card on T3N 6. Verify the registration All commands that talk to the network accept `--env testnet|production` (or the `T3N_ENV` environment variable). Use `--env testnet` while you're building — the examples below do. The CLI is part of the TypeScript SDK. You can run it with zero install, or install it globally: ```bash theme={null} # zero-install — always runs the latest published version npx @terminal3/t3n-sdk --help # or install globally (Node.js >= 18) pnpm add -g @terminal3/t3n-sdk t3n --help ``` The rest of this guide uses the global `t3n` form; substitute `npx @terminal3/t3n-sdk` if you skipped the install. The agent's identity key is a standard Ethereum-style **secp256k1 private key** (32 bytes, `0x`-prefixed hex). Registration is a write operation that consumes credits, and an agent's credit balance is separate from its tenant's — so get this key from the [claim page](/developers/adk/get-started/prerequisites/request-test-tokens), the same self-serve page you used to claim your own key: it issues a fresh key together with metered test credits every time, so you can revisit it once per agent, not just once for yourself. A key generated any other way (there is no `t3n keygen` command, but any tool that generates an Ethereum keypair produces a technically-usable one) starts with **zero** credits and can't pay for this step. Put the key in your shell's environment; every signing command reads it from there (or from `--api-key`): ```bash theme={null} export T3N_API_KEY="0x" ``` The key never leaves your machine — the CLI uses it locally to sign a login challenge. Treat it like any private key: keep it out of source control, and give each agent its **own** key. Never reuse your tenant developer key for an agent (see [Member Delegation](/developers/adk/get-started/member-delegation)). A T3N DID has the form `did:t3n:<40 hex characters>`. The network binds it to your key the first time you authenticate — so you don't compute it yourself, you read it back: ```bash theme={null} t3n whoami --env testnet # did:t3n:1a2b3c... export AGENT_DID=$(t3n whoami --env testnet) ``` **Always read the DID back from `t3n whoami` (or `--json` for `{"did": "..."}`).** Never hard-code a DID or try to derive it from the key locally — the canonical value is the one the network returns for your authenticated session. The agent card is a JSON document, following the [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) registration format, that describes your agent and lists its service endpoints — including its [A2A](https://a2a-protocol.org/) agent card. Scaffold one with: ```bash theme={null} t3n agent create-card --did "$AGENT_DID" ``` Run interactively, it walks you through name, description, image, and which services to include (A2A / MCP / DID). In scripts or CI it falls back to flags: `--out ` (default `agent-card.json`), `--name`, `--description`, `--image`, `--did`, `--x402`, and `--force` to overwrite an existing file. The generated `agent-card.json` looks like this: ```json theme={null} { "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1", "name": "My T3N Agent", "description": "REPLACE: what your agent does", "services": [ { "name": "A2A", "endpoint": "https://agent.example/.well-known/agent-card.json", "version": "0.3.0" }, { "name": "MCP", "endpoint": "https://mcp.agent.example/", "version": "2025-06-18" }, { "name": "DID", "endpoint": "did:t3n:REPLACE_WITH_YOUR_DID", "version": "v1" } ], "x402Support": false, "active": true, "registrations": [], "supportedTrust": ["tee-attestation"] } ``` Before moving on, replace the placeholders: point the `A2A` service endpoint at your agent's real `.well-known/agent-card.json` URL, the `MCP` endpoint at your MCP server (or remove services you don't offer), and make sure the `DID` service endpoint is your `$AGENT_DID` (the `--did` flag fills this in for you). Keep the whole card under **16 KiB** — that's the limit T3N accepts when it hosts the body in the next step. T3N hosts the card for you — no external bucket, pinning service, or web server required. One command stores your `agent-card.json` and publishes it to a world-readable endpoint keyed by your DID: ```bash theme={null} t3n agent host-card --file agent-card.json --env testnet # card published: https:///api/agent-card/did:t3n:1a2b3c... ``` That's it — your card is now served, verbatim, at `GET /api/agent-card/`. `host-card` does two things in one transaction: it stores the card **privately** under your DID, then publishes a public copy. Update it any time by editing the JSON and re-running `host-card`; take it down with `t3n agent card-unpublish`. T3N validates and serves the card **body** itself: it must be a single JSON object of at most **16 KiB**, and it is served byte-for-byte at `GET /api/agent-card/`. This is the T3N-hosted path — you no longer need to host the JSON anywhere else. **Registering just a URI instead.** If you'd rather keep the card on your own domain (per the ERC-8004 model), `t3n agent set-card --uri ""` records that URL in your DID document's `AgentService` endpoint. As a convenience it also publishes a minimal **default** T3N card for your DID so you're immediately discoverable — pass `--no-card` to skip that, or run `host-card` afterwards to replace the default with your full card. Existing cards are never overwritten. `host-card` opens an authenticated session with your agent key and runs two writes on the built-in `tee:org-data/contracts` TEE contract: it stores the card as ordinary org-data in your **self-owned** `agent-cards` scope (private, keyed by a deterministic entry id for your DID), then `agent-card-publish` copies that body into the world-readable `public:agent_cards` map the endpoint reads. You can only host or publish a card for your **own** DID; an organisation hosting on behalf of its agents uses `--owner`/`--agent` and a consent link (see [Member Delegation](/developers/adk/get-started/member-delegation)). Resolution is public — anyone can fetch your card without a key. Hit the endpoint directly: ```bash theme={null} curl https:///api/agent-card/"$AGENT_DID" # → your agent-card.json, served verbatim ``` If you also registered a URI with `set-card`, it appears in your DID document's `AgentService` endpoint, resolvable through the public DID resolver (`GET /api/did/`): ```bash theme={null} t3n agent registry "$AGENT_DID" --env testnet # id: did:t3n:1a2b3c... # agent: https:///api/agent-card/did:t3n:1a2b3c... ``` ```json theme={null} { "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/secp256k1recovery-2020/v2" ], "id": "did:t3n:1a2b3c...", "verificationMethod": [ { "id": "did:t3n:1a2b3c...#eth-owner", "type": "EcdsaSecp256k1RecoveryMethod2020", "controller": "did:t3n:1a2b3c...", "blockchainAccountId": "eip155:1:0x" } ], "authentication": ["did:t3n:1a2b3c...#eth-owner"], "service": [ { "id": "did:t3n:1a2b3c...#agent", "type": "AgentService", "serviceEndpoint": "https:///api/agent-card/did:t3n:1a2b3c..." } ] } ``` Consumers verify ownership through the DID document itself: the `verificationMethod` binds the DID to the owner's key, and the card body is served at the T3N endpoint above. `t3n did get ` prints the same view, and `t3n agent registry --full` (authenticated) reads the full registry record directly from the contract. If the endpoint returns your card, you're done — your agent is registered and discoverable on T3N. ## Deregistering your agent An agent can remove its own registry footprint — its registry record and any cards it hosted — at any time: ```bash theme={null} t3n agent deregister "$AGENT_DID" # agent deregistered: did:t3n:1a2b3c... ``` Omit the DID to act on the caller's own authenticated session (`t3n agent deregister`). This is self-service only: it removes the *calling* agent's own registration, not another agent's — a registry record that names an owner (i.e. an org-owned agent, see [Register an Organization-owned Agent](/developers/agents/provision-org-agent)) refuses deregistration through this path. Needs `tee:agent-registry/contracts` ≥ 1.7.0. The equivalent SDK call is `client.deregisterAgent(agentDid?)` — see the [SDK & API Reference](/developers/adk/reference). ## What's next * The agent now has an identity and is discoverable. Next, let it actually *do* something — granting permission to act on users' data and contracts → [Member Delegation](/developers/adk/get-started/member-delegation) and [Delegate Access](/t3n/data-owner-guide/delegate-access) * How DIDs work on T3N → [Decentralized Identifiers](/t3n/how-t3n-works/did) * Registration writes consume credits → [Tokens](/t3n/how-t3n-works/tokens) * Full method/CLI history for agent create/fund/update/delete/deregister → [Changelog](/developers/adk/changelog) # About Source: https://docs.terminal3.io/intro/about-t3 Terminal 3 is a **data freedom company**. We want to empower a more **equitable digital future**, where users and enterprises have equal rights and protections across all platforms. Our technology makes **fully private data freely composable**, securing the world's most important asset while realizing its full value. Terminal 3 powers identity and trust infrastructure across enterprises, governments, and Web3 platforms worldwide. ## Common Use Cases Verifiable identity management -- for humans and AI agents alike -- without the data risks, facilitating universal ID, cryptographic credentials, and private data storage. Interoperable KYC/AML credentials accepted across institutions, jurisdictions, and borders. Verify once, and share the proof, not the underlying data. Hardware-attested mandates for AI agents, so every agentic action is bounded, logged, and provable for increased security and privacy. Verifiable national ID credentials that can be accepted at borders, airline check-ins, and public service access points without physical document checks. # Decentralized ID (DID) Source: https://docs.terminal3.io/intro/components/did Decentralized Identities and their methods Terminal 3 provides universal identities for both humans and AI agents based on W3C’s [Decentralized Identifiers (DID)](https://www.w3.org/TR/did-core/) global standard specs. # What is a Decentralized Identifier (DID)? DIDs are identifiers that enable verifiable, decentralized digital identities. A DID refers to any subject (e.g. a person, group, organization, abstract entity, etc.) as determined by the controller of the DID. Specifically, DIDs are [URIs](https://www.rfc-editor.org/rfc/rfc3986) (unique digital addresses) that associate a DID subject with a DID document allowing trustable interactions associated with that subject. Similar to a URL, when you resolve a DID, you receive a DID document in JSON, JSON-LD, or similar format. ## A simple DID example A DID is a simple unique text string consisting of three parts: 1. the `did` URI scheme identifier; 2. the identifier for the DID method; and 3. the DID method-specific identifier. DID-method specifier identifier The example DID above resolves to a DID document. ## **Multiple DIDs** A subject may have as many DIDs as they want for different use cases, such as: * a DID for educational certificates; or * a DID for verified identity / KYC documents; or * a DID for online gaming accounts. # What is a DID Document? A DID document contains information associated with the DID, such as ways to cryptographically authenticate a DID controller. ```json theme={null} { "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1" ] "id": "did:example:123456789abcdefghi", "authentication": [{ "id": "did:example:123456789abcdefghi#keys-1", "type": "Ed25519VerificationKey2020", "controller": "did:example:123456789abcdefghi", "publicKeyMultibase": "zH3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV" }] } ``` # DID Methods A DID method (and its spec) defines the precise operations by which DIDs and DID documents are created, resolved, updated, and deactivated. There are currently more than [150](https://www.w3.org/TR/did-spec-registries/#did-methods) DID methods out there, which can be categorized into 4 main types based on their **storage method**: * Centralized: relies on a Web2 server * Blockchain-based: the “original” DID method involving a blockchain * Hybrid: uses decentralized storage to store its documents * Static: no data registry is required; can be created and resolved by encoding/decoding data directly. Terminal 3 currently provides a custom `did:t3n` DID. | | Centralized | Blockchain-based | Hybrid | Static | | ------------------------- | ---------------------------------------- | ---------------------------------------- | ---------------------------------------- | -------------------- | | Privacy | low | high | high | medium | | Self-Sovereign? | no | yes | yes | yes | | Data Registry Required? | yes | yes | yes | no | | (web2 server) | yes | | | | | (Blockchain) | | yes | yes | | | (L2 storage) | | | yes | | | Decentralized? | no | yes | yes | yes | | Complexity | simple | complex | medium | simple | | Transaction Fee Required? | no | yes | yes | no | | (when updating) | | | yes | | | Mutable? | yes | no | no | no | | Supported Operations | create
read
update
delete | create
read
update
delete | create
read
update
delete | create
read | | Example | did:web | did:ethr
did:btcr
did:dock | did:3
did:ion
did:elem | did:key
did:pkh | # DID System Architecture ### Generic DID Architecture Generic DID architecture # Smart VCs Source: https://docs.terminal3.io/intro/components/vc Verifiable Credentials Credentials are a regular part of our lives. For example, university degrees, passports, and certifications are all examples of credentials that assert some claim about us. Based on W3C’s [Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/) specs, Terminal 3 provides a set of REST APIs and SDKs for the issuance and verification of digital credentials that are cryptographically secure, privacy-preserving, and machine-verifiable. # Claims A **claim** is a statement about a **subject**. A **subject** is a thing (typically a person) about which **claims** can be made. **Basic structure of a claim:** ```mermaid theme={null} flowchart LR subject([Subject])-- Property -->value[value] ``` **A claim expressing that Terry is an alumni of "OC University”:** ```mermaid theme={null} flowchart LR student([Terry])-- alumniOf -->university[OC University] ``` **Multiple claims can be combined to express a graph of information:** ```mermaid theme={null} flowchart LR student([Terry])-- alumniOf -->university[OC University] student-- knows -->professor([Gary])-- jobTitle -->job[Professor] ``` # Verifiable Credentials (VC) A **credential** is a set of one or more **claims** made by an **issuer**. A **verifiable credential (VC)** is a tamper-evident credential and metadata that cryptographically proves who issued it. "Verifiable" means a credential can be verified by a **verifier**. It only implies that an issuer signed the VC; it does *not* imply the truthfulness of claims. A VC typically includes credential metadata, claim(s), and proof(s). Specifically, a VC is a hash presentation in either [JSON](https://www.json.org/json-en.html) or [JSON-LD](https://json-ld.org/) format. Here is an example of a VC in JSON-LD: ```json theme={null} { "@context": [ "https://www.w3.org/2018/credentials/v1" ], "id": "https://app.terminal3.io/credentials/58473", "type": ["VerifiableCredential", "UniversityDegreeCredential"], "issuer": "did:key:terminal3", "issuanceDate": "2023-01-01T00:00:00Z", "credentialSubject": { "id": "did:ethr:0xebfeb1f712ebc6f1c276e12ec21", "degree": { "type": "BachelorDegree", "name": "Bachelor of Science and Arts" } }, "proof": { ... } } ``` Notes: * [DIDs](/documentation/preliminaries/web-standards/dids) are used to represent credential subjects * Proofs establishing that the issuer generated the credential are required for a VC/VP to be verifiable * To maximize data privacy and security, VCs are typically stored off-chain # Verifiable Presentations (VP) A VP expresses data derived from one or more VCs, issued by one or more issuers, that is shared with a specific verifier. It is a tamper-evident presentation encoded in such a way that authorship of the data is cryptographically verifiable. VPs consist of presentation metadata, VC(s), and proof(s). We also leverage zero-knowledge (ZK) proofs to enable selective disclosure in VPs. A verifiable presentation example # Triangle of Trust There are three primary roles — known as the Triangle of Trust — that make up a VC ecosystem: *issuers*, *holders*, and *verifiers*. ```mermaid theme={null} flowchart TB subgraph T[ ] direction LR issuer[Issuer]-- issue VCs -->holder[Holder]-- present VP -->verifier[Verifier] end subgraph DR[ ] registry[Verifiable Data Registry] end T-- use identifiers and schemas -->DR ``` ### Issuer * Asserts claims about one or more subjects, and creates VCs from these claims * Examples: enterprises, organizations, and governments ### Holder (Subject) * The entity (typically a user) about whom a claim is issued * Acquires and possesses one or more VCs, and consents to when, and with whom, they are shared * May bundle VCs to generate VPs * Holders may either self-custody VCs themselves, or store VCs in Terminal 3’s decentralized storage * Examples: consumers and end users ### Verifier * Receives one or more VCs, usually inside a VP, for processing and verification * Checks that VCs were issued by a legitimate issuer * This is done via on-chain DID Resolution; the signature and signing keys in the VC must match the issuer * This implies that the Issuer and Verifier do not need to have a direct relationship with each other * Checks that VCs have not been tampered with, expired, nor been revoked * Examples: banks, employers, and applications ### Verifiable Data Registry * A system that mediates the creation and verification of identifiers, keys, and other relevant data, such as VC schemas, revocation registries, issuer public keys, etc. which might be required to use VCs. * Example: blockchains and distributed ledgers. # VC Lifecycle The lifecycle of a VC (and VP) can be simplified into the following journey: Issuers publish their public keys in an on-chain DID Registry, managed by Terminal 3.
A DID is used to hold the Issuer’s public key
Create a VC and digitally sign it with their private cryptographic key Encrypt the VC - Store the encrypted VC in an off-chain Credential Repository, managed by Terminal 3 Request the VC from Holder - Extract DID from the VC - Extract the Issuer’s public key
Use the Issuer’s public key to verify:
1. The Issuer has the authority to issue the VC, via the DID Registry
2. The VC is still valid (not expired nor revoked, via the Revocation Registry)
# Can VCs be revoked or deleted? * Issuers can *revoke* a VC, by posting to an on-chain Revocation Registry via our SDK * Holders can *delete* a VC # Platform Overview Source: https://docs.terminal3.io/intro/platform The Terminal 3 (T3) trust infrastructure platform is a comprehensive solution for developers and enterprises managing users and agents at scale, with privacy at its core. ## T3 Network (T3N) At the foundation, the [T3 Network (T3N)](/t3n) is a confidential compute network for all your most important data. Store, process, and compute on sensitive data inside hardware-secured Trusted Execution Environments (TEEs). Receive verified results and power secure agentic actions without raw data ever leaving the secure enclave. Built on top of T3N are Terminal 3's products that provide verifiable identity, privacy, and security for both humans and AI agents: * [T3 Identity](https://www.terminal3.io/products/t3-identity) * [T3 Verify](https://www.terminal3.io/products/t3-verify) * [T3 Agent Developer Kit (ADK)](/developers) * *T3 Agent Command (coming soon)* ## Human Identity and Privacy **T3 Identity** and **T3 Verify** are enterprise-grade products that let you onboard users, manage access, and engage verified audiences — without storing, handling, or exposing personal data. * **Portable identity across platforms:** One interoperable Decentralized ID (DID) that follows your users across products, partners, and jurisdictions. No re-onboarding, no data copies between systems. * **Store and process customer data without holding PII:** Store user profiles, credentials, and preferences securely and privately. Every process executes against encrypted data inside a TEE so applications receive answers, never raw records. * **Anchor and validate Smart Verifiable Credentials (VCs):** Issue portable claims once, verify everywhere. Anchor credentials in T3N's on-chain Issuer and Revocation Registries. Every credential presentation is validated against live registry state, so no credential reaches a platform without confirmation the issuer is trusted and the credential is unrevoked. ## AI Agent Security and Governance Discover, govern, and audit every AI agent across your organization. Runtime policy enforcement, sensitive data processed in hardware enclaves, and tamper-proof audit trails ready for any regulator. * **Runtime policy enforcement:** Define exactly what agents can access and do. Policies are evaluated at request time — not just at provisioning — so scope creep is blocked in flight. * **Tamper-proof audit trail:** Every agent action is cryptographically signed and logged to an immutable Merkle-backed ledger. Export ready for any regulator, any time. Give every agent a verifiable identity, scoped authorization, and hardware-secured payment execution. Drop in beside your existing agent with no architecture changes required. * Assign every agent a cryptographically verifiable DID. Portable across systems, provenance-linked to the authorizing human operator. * Sensitive data (e.g. payment credentials) stays in T3N, resolved inside a TEE, and never enter agent memory, context, or prompt history. # Delegate Access Source: https://docs.terminal3.io/t3n/data-owner-guide/delegate-access How to grant and manage access permissions for your data. One of T3N's most powerful features is the ability to securely share specific parts of your data without exposing your entire identity. You can delegate access to: * **AI agents:** A travel bot that needs to book flights for you. * **Human users:** A family member or financial advisor. * **Third-party services:** A loan application service checking your credit score. For more end‑to‑end stories on how delegation works in practice (for example, travel assistants and loan agents), see [Use Cases](/t3n/use-cases). ## Delegate Access to AI Agents To configure who can access your data: 1. Log in to the [T3N Dashboard](https://testnet.network.terminal3.io). 2. Click on the `AI Agents` tab. 3. Click on `New agent`. 4. Enter the `Agent DID`. 5. Select `Authorized TEE contract`. 6. \[Optional] Select authorized `functions`. 7. \[Optional] Input `Allowed hosts`. 8. Click `Add`. ### Definitions **TEE contracts:** are smart contracts that contain capabilities that an AI agent can act on your data on your behalf. For example, a TEE contract can check your credit score or book a flight for you. **Functions:** are the specific functions within a TEE contract that an agent can act on your behalf. For example, a TEE contract can have a function to check your credit score or book a flight for you. **Allowed hosts:** are the URLs that an agent can access. For example, you can allow a "Travel Agent" to only communicate with `https://api.airlines.com`. It cannot send your passport data to any other server. If optional fields are not specified, the agent will have access to all functions and hosts. ## Revoking Access You can revoke permissions at any time. 1. Log in to the [T3N Dashboard](https://testnet.network.terminal3.io/). 2. Click on the `AI Agents` tab. 3. Find the agent you want to revoke permissions for. 4. Click on `Remove`. ## Delegate Access to Human Users Coming soon. ## Delegate Access to Third-Party Services Coming soon. # Architecture Source: https://docs.terminal3.io/t3n/how-t3n-works/architecture A compliance-aware, multi-region, private-first architecture T3N is decentralized by design, offering resilience and censorship resistance while maintaining data privacy and strict regulatory compliance from day one. It achieves this through a multi-region topology that keeps regulated data inside its required jurisdiction, all while operating as a single, interoperable network. T3N is accessible through: * [Client SDK](/developers/adk/overview/what-is-adk) * MCP * APIs * Webhooks ## Topology ```text theme={null} +------------------------------------------------+ +-----------------------------------+ | REGION 1 | | REGION 2 | | (local data processing & residency required) | | (no data residency) | | | | | | +----------------------------------+ | | +-----------------------------+ | | | TEE Cluster 1 | | | | TEE Cluster 2 | | | | [Node 1] <--> [Node 2] |<---------|----->| | [Node 3] <--> [Node 4] | | | +----------------+-----------------+ | | +---------------+-------------+ | | ^ | | ^ | | | | | | | | v | | | | | +----------------+-----------------+ | | | | | | Regional Storages |<---------|------|------------------+ | | | [Content Addressable Storage] | | | | | | [Regulatory Vault] | | | | | +----------------------------------+ | | | | | | | +------------------------------------------------+ +-----------------------------------+ Connections ----------- Cluster 1 <--> Cluster 2 (cross-region threshold key sharing) Cluster 1 <--> Regional Storages (in-region storage) Cluster 2 <--> Regional Storages (remote access to Region 1 storages) ``` ## Trusted Execution Environment (TEE) Clusters TEE clusters are the computational layer of T3N. Each TEE cluster is geographically distributed and consists of multiple [TEE nodes](/t3n/how-t3n-works/tees). Each node holds a key share of the overall threshold key for the whole network. All confidential data processing is performed inside each node's Trusted Execution Environment (TEE) - secure computational enclaves within processors that deliver enhanced security beyond conventional operating systems. ## Regional Storage TEE nodes and storage are co-located within regions to meet data residency requirements. T3N is storage technology agonistic. Any storage solution can be integrated with T3N to serve different requirements. Currently,it supports two storage options: Content-Addressable Storage (CAS) and Regulatory Vault. ### Content-Addressable Storage (CAS) CAS is a storage system that stores data based on its content rather than its location. It's a pluggable external storage backend for large data values. ### Regulatory Vault The Regulatory Vault is secure, centralized storage managed by Terminal 3 for regulated partners. It securely holds sensitive KYC data (passports, liveness videos, etc.) specifically for compliance and enforcement purposes. By providing immutable auditability, it acts as a verifiable system of record to prove to regulators that all KYC requirements were met. # Consensus Protocol Source: https://docs.terminal3.io/t3n/how-t3n-works/consensus Building a resilient, tamper-evident encrypted ledger using Raft consensus T3N is composed of clusters of Trusted Execution Environment (TEE) nodes. The nodes use the Raft consensus protocol to agree on a single, ordered history of every state change. That history is an **append-only, encrypted ledger**; the current state (the KV store) is a fast projection rebuilt from it. Each committed change is folded into a SHA-256 Merkle tree and periodically checkpointed with a signed seal written inside the enclave, so anyone with access can cryptographically prove that a record existed and has never been altered. ## Why consensus at all? A single node can lie, crash, or lose data. T3N replicates every change across several independent TEE nodes so that: * **No single node is the source of truth** — a majority of nodes must agree before a change is considered committed. * **A cluster survives node failures** — the cluster keeps operating as long as a majority of nodes are healthy (meaning it can tolerate up to **⌊(n−1)/2⌋** failed nodes, where n is the total number of nodes in the cluster). For example, a 3-node cluster can tolerate **1** node failure, remaining operational as long as **2** nodes are healthy. * **State is consistent** — every healthy node applies the same changes in the same order. ## Raft consensus protocol T3N implements **[Raft](https://raft.github.io/raft.pdf)** from scratch, modeled directly on Ongaro's formal specification. It keeps the core safety and quorum mechanics of Raft intact, hardens the election path (pre-vote + CheckQuorum), and replaces Raft's plaintext disk log with an encrypted, Merkle-verifiable, enclave-signed ledger. Key mechanics are as follows: ### Leader and followers At any given time, one node is the **leader** and the others are **followers**. All writes flow through the leader, which decides the order of changes. Followers replicate the leader's log and can take over if the leader disappears. ### Leader election (with pre-vote) If followers stop hearing from the leader (an election timeout elapses), a new election begins. T3N uses a **pre-vote** round first, in which a node checks whether it can win before disrupting the cluster. This prevents two well-known problems: a disconnected node repeatedly inflating the election term, and a briefly partitioned node needlessly unseating a healthy leader. In a single-node cluster, the node self-elects on its first election timeout, so it becomes leader without any round trips. ### Replicating a change 1. A client request reaches the leader. 2. The leader **appends the change to its own local ledger first**. 3. The leader replicates the change to followers via **AppendEntries** messages (also used as heartbeats). 4. Once a **quorum** (e.g., 2 out of 3) of nodes has stored the change, the leader marks it **committed** and advances the commit index. 5. Committed changes are applied to the KV state on every node. The commit rule is genuine majority-quorum Raft: a change is only committed once a majority of nodes hold it, making it durable even if a node fails. ## The ledger: an append-only, encrypted history Consensus decides the order of changes; the **ledger** is where those changes are stored. It is T3N's authoritative, append-only record. | Layer | What it is | Analogy | | ------------ | -------------------------------------------------------------- | ------------------------------------------------ | | **Ledger** | Permanent, append-only, encrypted **history** of every change. | The accounting *journal* — every entry, forever. | | **KV store** | The **current value** of each key, held in memory. | The account *balances* — only "now". | Key properties: * **Append-only.** Entries are added, never edited in place. * **Each entry is split into two domains:** * **Public metadata** — readable without decryption, including integrity digests, plus the term and log-index that the entry occupies (carried in the entry's cleartext header). * **Encrypted payload** — the actual write-set (the keys and values that changed), protected with **AES-256-GCM**. * **Chunked storage** — entries are written to a write-ahead log (WAL) and rolled into immutable sealed chunks, persisted through a pluggable storage backend (e.g., local disk). * **Deterministic recovery** — because the ledger is the complete, ordered history, the entire KV state (and its indexes) can be rebuilt by **replaying** it from the start. A node that has lost its in-memory index can recover by replay. ## Tamper-evidence: Merkle tree + signed seals Being append-only is not, by itself, proof that nothing has changed. This proof is provided by the following tamper-evident mechanisms: * **Merkle tree.** Every committed entry becomes a **SHA-256** leaf in an append-only Merkle tree. The tree produces a single **root hash** that summarizes the entire history; if any past entry is changed, the root hash changes. * **Inclusion proofs.** For any entry, the cluster can produce a Merkle **proof path** from that leaf to the root, allowing a holder to verify that a specific record is part of the committed history without seeing every other entry. * **Seals (signed checkpoints).** Periodically, the cluster writes a **seal** entry that records a Merkle-root checkpoint. This checkpointing happens **inside the enclave (TEE)**, which allows a verifier to trust that the root was produced by genuine cluster code and has not been tampered with afterward. * **Commit evidence.** Each committed transaction also has deterministic **commit evidence** derived (via HMAC-SHA256) from a secret only the enclave holds, meaning the evidence can be re-derived during audit or recovery but cannot be forged by an outsider. Together, these mechanisms prove that *a record exists*, *where it sits in history*, and *that the history has not been rewritten*. ## What Raft consensus does *not* do * **It does not make data readable by node operators.** Payloads are encrypted; consensus replicates ciphertext, and decryption happens inside the TEE. * Raft is a *crash-fault-tolerant* protocol: it assumes nodes are honest but may fail. The protection against a malicious operator comes from the **TEE + encryption + tamper-evidence**, not from the consensus algorithm itself. * **Availability requires a majority.** With n nodes, losing more than **⌊(n−1)/2⌋** nodes at once halts new commits until a majority is restored. # Data Encryption Source: https://docs.terminal3.io/t3n/how-t3n-works/data-encryption Private data is always encrypted in transit and at rest T3N implements robust encryption for both data in transit and data at rest, leveraging post-quantum cryptography, Trusted Execution Environments (TEEs), and industry-standard symmetric encryption algorithms. ## Encryption in transit ### Client-to-node traffic T3N uses two layers of encryption to protect data in transit: 1. **HTTPS** protects the client connection to the deployment’s public load balancer. 2. Core **Session API** paths add application-layer encryption. The client and node establish a shared secret with ML-KEM-768, derive directional keys with HKDF-SHA256, and encrypt payloads with AES-256-GCM. Once the ML-KEM public key and an expected runtime measurement obtained through a trusted, out-of-band channel have been verified, the application-layer channel limits plaintext exposure to the client and the TEE. Without that verification, the client only knows that it encrypted to the key returned by the node's `/status` endpoint. The T3N SDK performs this verification via `fetchTrustedManifest()` — see [Verify the trust anchor](/developers/adk/tips/verify-trust-anchor) for how a client obtains and checks the out-of-band manifest in practice. In some cases (e.g., trusted B2B integrations), session-authenticated endpoints only rely on HTTPS and trusted party's specific authentication (e.g, HMAC, EOA signatures, and bearer tokens). ### Handshake An end-to-end encrypted channel is established using ML-KEM (FIPS 203): ```mermaid theme={null} sequenceDiagram participant C as Client participant TEE as TEE Node C->>C: ML-KEM.encaps(trinity_public_key) → (K, ciphertext) C->>TEE: auth.handshake(ciphertext) TEE->>TEE: ML-KEM.decaps(secret_key, ciphertext) → K TEE->>TEE: Generate session_id, store {key: K, expiry} TEE-->>C: Session-Id header + expiry (TLS-protected) ``` The shared secret K is never transmitted: the client derives it during encapsulation and the node recovers it by decapsulating inside the TEE. The handshake response itself carries only the session identifier and expiry — a lookup handle with no key material — protected by HTTPS like the rest of the exchange. The node does not contact other nodes during each client handshake to jointly decrypt the ML-KEM ciphertext. Instead: 1. During startup, the node participates in the threshold process and reconstructs/recovers the wrapping secret key. 2. It keeps that key inside the TEE. 3. For each handshake, it uses the already-recovered key locally. “Threshold” protects key provisioning/recovery at startup, but handshake decapsulation itself is fast and node-local. Both client and server derive two keys from the ML-KEM shared secret: * `t3n-session-v1-c2s`: client encrypts, server decrypts. * `t3n-session-v1-s2c`: server encrypts, client decrypts. Using different keys for each direction prevents a sender from decrypting its own outgoing ciphertext with its receive key. Sessions are node-affine: the node that handles the handshake stores the session state in memory, and later requests in that session must return to that node. ### Requests and responses For paths that use Session API application-layer encryption, every encryption operation: 1. Generate a fresh random 12-byte nonce 2. Encrypt the payload with AES-256-GCM 3. Prepend the nonce to the ciphertext 4. Verify the authentication tag during decryption Changing the nonce, ciphertext, or authentication tag causes decryption to fail. ### Node-to-node traffic Node-to-node traffic first uses libp2p Noise for an encrypted, peer-authenticated channel. T3N then applies a TEE protocol upgrade in which peers: 1. Exchange fresh challenges 2. Bind their libp2p identity to a TEE attestation quote 3. Verify the quote and approved TEE measurements 4. Compare a Keccak-256 digest of the cluster configuration A peer is rejected when its quote matches no effective trusted measurement. The effective set includes the node's own startup measurement, even when the configured allow-list is absent or empty. [Raft](/t3n/how-t3n-works/consensus) traffic and key-recovery messages use this attested channel. ## Encryption at rest ### Private ledger data T3N stores application state in named key-value collections called **maps**. Every committed transaction is recorded in the replicated ledger as a set of changes to those maps. Each ledger entry separates these changes into: * a **public domain**, containing changes to maps whose names start with `public:` * a **private domain**, containing changes to all other maps The private domain is encrypted using AES-256-GCM with the 32-byte ledger encryption key. Each committed entry is identified by its [Raft](/t3n/how-t3n-works/consensus) term and log index. T3N encodes this pair into the entry’s unique 12-byte encryption nonce. Conceptually, ``` nonce = derive_nonce(term=7, index=42) ciphertext = AES-256-GCM(ledger_secret, nonce, private_data, public_data) ``` The entry’s wire header and complete public domain are authenticated but not encrypted. They remain readable, but modifying either will cause authentication to fail when the private domain is decrypted. Public maps are intentionally stored as plaintext in the replicated ledger. They must not contain personally identifiable information or other secrets. ### Private binary large objects (blobs) Content addressable storage (CAS) can be configured for storing large binary objects: 1. Compute a SHA-256 content identifier (CID) from the plaintext; 2. Encrypt the blob with AES-256-GCM under the CAS content encryption key (CEK); 3. Store the blob under the CID-derived object key; and 4. Store a `ValueRef` in the KV map. `ValueRef` records the CID, size, storage location, encryption status, and last-touch time. On read, T3N decrypts encrypted blobs and verifies that the plaintext hash matches the CID. ## Quick reference | Area | Current implementation | | ---------------------------- | ------------------------------------------------------------------------------------------ | | Client session establishment | ML-KEM-768 | | Session key derivation | HKDF-SHA256 with separate client-to-server and server-to-client keys | | Session payload | AES-256-GCM with a fresh random 12-byte nonce per message | | Node-to-node transport | libp2p Noise plus mutual Intel TDX attestation and a Keccak-256 configuration-digest check | | Private data in the ledger | AES-256-GCM | | Private blobs | AES-256-GCM with a separate CAS Content Encryption Key (CEK), when encryption is enabled | # Decentralized Identifiers (DIDs) Source: https://docs.terminal3.io/t3n/how-t3n-works/did Universal ID for both human and agent [DIDs](https://www.w3.org/TR/did-1.1/) are a core component of the W3C's Verifiable Credentials standard and enable secure, private, and verifiable digital identity.When you first onboard to the T3N Network, the network automatically generates your unique `did:t3n` identity. Every entity in the network whether a human user or an AI agent is assigned a unique Decentralised Identifier (DID). The format of a DID in T3N is: `did:t3n:`. Key properites and uses include: * It's a permanent address on the network, linked to authentication methods and permissions. * Used to access and manage your data and TEE contracts. * Only binded to one user profile or agent. * Each DID can hold a T3N [token](/t3n/how-t3n-works/tokens) balance. # Host API Source: https://docs.terminal3.io/t3n/how-t3n-works/host-api Host API grants TEE contracts controlled access to system capabilities ## Overview TEE contracts execute as WebAssembly (WASM) components within a sandboxed Wasmtime runtime hosted by a T3N node. By default, contracts have no direct access to the operating system, network, filesystem, clock, randomness, or other system resources. To minimize the attack surface and enforce least-privilege access, TEE contracts interact with the outside world exclusively through a strongly typed Host API implemented by the T3N node using the WASM Component Model. This capability-based model ensures that contracts can access only the resources and functionality explicitly exposed by the host runtime. If a capability is not defined in the Host API, the contract cannot do it. ## Host interfaces TEE contracts can only interact with the outside world through explicitly defined host interfaces, and all interfaces are gated by authorisation checks — a contract cannot access capabilities it has not been granted. | Interface | What it does | Use it when | Gating | [z-namespace](/t3n/how-t3n-works/z-namespace) | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | | `kv-store` | Read / write / delete entries in the contract's namespaced key-value maps. Writes participate in the same atomic transaction as the rest of the call. | Persisting contract state. | Namespace is bound to the contract; no cross-contract reads. | ✅ | | `tenant` | Tenant-scoped lifecycle and metadata reads. | Multi-tenant contracts that need to know which tenant they're running under. | Standard per-contract authorisation. | ✅ | | `logging` | Emit `info` / `debug` / `error` log lines. | Diagnostics. | None. | ✅ | | `http` | Outbound HTTP / HTTPS requests. | Calling a third-party API from your contract. | Per-contract egress allowlist of host names. | ✅ | | `http-with-placeholders` | Outbound HTTP where the host substitutes `{{profile.}}` markers in headers / body from the calling user's profile before sending. The plaintext value never enters WASM. | Sending user PII (email, phone) to an upstream API without giving the contract read access to it. | Egress allowlist (shared with `http`) plus a per-contract `placeholder_allowlist` naming which profile fields may be substituted. | ✅ | | `signing` | Three signing primitives: (1) cluster ECDSA — Keccak-256 + secp256k1 — via `sign` / `sign-as-user`; (2) per-wallet signing via `sign-with-wallet` for any wallet the calling DID owns; (3) SD-JWT Verifiable Credential issuance via `sign-sd-jwt-vc` (ES256 / P-256). `sign-as-user` produces an EIP-191 personal-sign signature (host applies the prefix); the contract passes raw bytes. | Producing an Ethereum-compatible signature, signing a transaction on behalf of a user, or issuing a Verifiable Credential. | Per-contract allowlist on `sign-as-user`. `sign-with-wallet` additionally verifies the wallet address is in the caller's wallet set. | Coming soon | | `outbox` | Enqueue an HTTP request to be delivered after the current transaction commits. Delivery is at-most-once with an idempotency-key dedupe window, driven by a single leader. | Triggering an external side-effect (payment, webhook) that must not double-fire if the contract is replayed. | Per-contract allowlist of upstream hosts. | ✅ | | ~~`ap2` (`ap2/mandate`)~~ | **Removed in `testnet-v1.0.10`.** Used to build and verify AP2 payment mandates; the host interface, its linker world, and the KV policy row backing it were all deleted from the runtime. A contract that still imports `ap2/mandate` will fail to instantiate. No replacement has shipped as of this release — if you were relying on AP2 mandates, reach out on the [developer Telegram](https://t.me/terminal3developer). | — | — | — | | `vp` | Sign and submit Verifiable Presentations (OID4VP). Verifier accepts SD-JWT + Key Binding envelopes against a trusted-issuer registry. | Issuing or verifying W3C-style Verifiable Credentials. | Trusted issuers maintained by the cluster operator. | Coming soon | | `did-registry` | Link an authenticator (email, OIDC subject, …) to a Decentralised Identifier on chain. | Onboarding a user with a new login method. | Standard per-contract authorisation. | Coming soon | | `agent-auth` | Update which TEE contracts / functions an agent is authorised to invoke. | Managing delegated agent permissions. | Standard per-contract authorisation. | Coming soon | | `user-profile` | Encrypt and store user profile data. | Persisting PII alongside a DID. | Standard per-contract authorisation. | Coming soon | | `user-removal` | Remove all data associated with a user. | GDPR / account deletion flows. | Admin-only by default. | Coming soon | | `contracts-call` | Synchronously call another contract's exported function. The inner call executes inside the same transaction as the outer call; on inner failure the inner writes are discarded but the outer call may continue. | Composing contracts (e.g. a workflow contract calling a payment contract). | Per-pair `(caller, target)` allowlist with `allowed_functions` and `max_depth`. Reentrancy and depth overruns return typed errors (`reentrant`, `depth-exceeded`). | System-only | | `stash` | Drain the binary blob attached to the current multipart request into content-addressed storage and hand the contract a reference to it. | Accepting a file or large payload uploaded alongside the contract call. | Per-contract allowlist; default deny. Single-use per request — a second call returns `AlreadyConsumed`. | System-only | | `agent-registry` | Register or update the agent URI for a DID. | Publishing where a user's agent is reachable. | Standard per-contract authorisation. | System-only | | `authorisation` | Ask the host whether a given action / host is currently permitted. | Pre-flighting a sensitive operation before doing the work. | None (pure read). | System-only | | `otp` | Verify a one-time password and track its status. | Email / SMS code verification flows. | Standard per-contract authorisation. | System-only | | `config/read` | Read a cluster-level configuration value by key (typed `read-u64` / `read-string`). | Reading a non-secret cluster knob set by the operator. | None — configs are public metadata. | System-only | | `provider-config` | Read operator-managed provider configuration and, if the contract is on the matching secret row's allowlist, the merged secret bag. | Calling an external provider (KYC, OAuth) whose API key the operator has configured. | Per-contract allowlist on each secret row. | System-only | | `time` / `clock` | Read wall-clock and monotonic time. | Any time-dependent logic — there is no other source of time inside WASM. | None. | System-only | | `node-config` | Read node-level configuration values exposed to contracts. | Reading host-published runtime knobs. | None. | System-only | # Trusted Execution Environment (TEE) Node Source: https://docs.terminal3.io/t3n/how-t3n-works/tees Confidential, programmable, and verifiable computation ## Overview A T3 Network (T3N) node, also referred to as a TEE node, is a participant in the T3N peer-to-peer network that runs inside a hardware-backed Trusted Execution Environment (TEE). The TEE provides confidential, tamper-resistant, and remotely attestable execution, ensuring that workloads run as intended and that data remains protected from node operators. Applications on T3N are packaged as TEE contracts. Each contract is executed by the node's WASM runtime (**[Wasmtime](https://wasmtime.dev/)**) in a sandbox that limits it to a set of granted [host functions](/t3n/how-t3n-works/host-api), and the whole runtime executes inside the TEE. T3N nodes provide the runtime environment required to run these contracts, including: * Post-quantum encrypted communication channels * Authentication and attestation services * Consensus-backed storage (a distributed key-value store replicated across the cluster using the Raft consensus protocol and protected with Merkle-tree integrity proofs) * Host functions that allow TEE contracts to interact with external systems and network services All communication with T3N nodes is end-to-end encrypted. Data can only be decrypted within attested TEEs through T3N’s threshold encryption mechanism, preventing node operators, infrastructure providers, and other unauthorized parties from accessing sensitive information. The T3 Network currently utilizes **Intel TDX (Trust Domain Extensions)** to run Secure Encrypted Virtual Machines. Future versions will support other TEEs. ## TEE Contract A TEE contract is a WebAssembly (WASM) binary that executes within a WASM sandbox hosted by a T3N node. It enables developers to run application-specific logic on private data inside the TEE. TEE contracts are stored off-chain in content-addressable storage (CAS) and are not persisted directly on T3N nodes. Instead, the node registry maintains references to contract artifacts, allowing nodes to retrieve and execute the correct contract version on demand. A TEE contract is invoked only after the requested data has been securely decrypted within an attested TEE and all required authentication and authorization checks have been successfully completed. TEE contract processing (Light mode) TEE contract processing (Dark mode) ## Execution Flow The execution flow within a T3N node ensures that data, code, and computation remain secure and isolated. The contract execution relies on two primary data sources: the parameters from the user’s request and the user’s private data fetched from the storage(i.e., CAS). For example, when a client requests the execution of a TEE contract on user data, the node orchestrates a secure process: decrypting the request, fetching and decrypting both the user's data and the contract code, executing the logic within a sandbox, and returning an encrypted response. The sequence diagram below illustrates this lifecycle: ```mermaid theme={null} sequenceDiagram participant C as Client participant TEE as TEE Node participant CAS as CAS (External Storage) participant KV as KV Store C->>TEE: action.execute(encrypted request) TEE->>TEE: Decrypt request → {tee_contract_name, function_name, input} TEE->>KV: Get user profile ValueRef by DID KV-->>TEE: ValueRef (CID, storage location) TEE->>CAS: Fetch encrypted profile blob by CID CAS-->>TEE: Encrypted profile blob TEE->>TEE: Decrypt profile (AES-256-GCM with cluster CEK) TEE->>KV: Resolve TEE contract ValueRef from registry KV-->>TEE: TEE Contract ValueRef TEE->>CAS: Fetch TEE contract by CID CAS-->>TEE: TEE contract TEE->>TEE: Execute TEE contract (profile, input) in sandbox TEE-->>C: Encrypted response ``` ## Steps The client sends an encrypted execution request to a TEE Node. Inside the secure enclave, the TEE decrypts the request to identify the target TEE contract, function, and input parameters. The TEE queries the Key-Value (KV) Store using a Decentralized Identifier (DID) to locate the user's data. It retrieves a reference (CID) and fetches the encrypted data blob from external Content Addressable Storage (CAS). The TEE securely decrypts the user's data using the cluster's encryption key (CEK). The TEE resolves the requested TEE contract's location from the KV Store registry and fetches the corresponding TEE contract bytecode from CAS. The TEE executes the TEE contract in an isolated sandbox, providing it with the decrypted user data and input data. Finally, the execution result is encrypted and returned to the client. # Tokens Source: https://docs.terminal3.io/t3n/how-t3n-works/tokens How T3N tokens meter TEE contract execution and storage ## Overview T3N tokens are used to meter TEE contracts execution and storage inside the T3N. They are not OAuth tokens, session tokens, JWTs, or API keys. Every T3N identity is represented by a [DID](/t3n/how-t3n-works/did), and each DID can hold a T3N token balance. When a DID performs a metered operation, the operation and its associated token charge are processed as a single atomic transaction. This guarantees that the work and the charge either succeed together or fail together. If the operation is successfully recorded, the corresponding tokens are deducted. If the operation fails, no tokens are charged. As a result, work cannot be recorded without payment, and payment cannot be deducted without the corresponding work being recorded. At a high level: ```text theme={null} Your DID balance | | pays for v Metered contract execution - WASM compute fuel - host-function calls such as kv.put or cas.write - storage deposit and storage rent - contract registration when the admin request bills your DID ``` Tokens are currently non-transferable. Transferrability is coming soon. ## Failure Semantics Metered WASM uses charge-on-attempt semantics. If a contract starts and consumes cluster resources, the caller pays for the fuel and host-function budget consumed even if the contract later returns an error, panics, or runs out of token. Contract writes are rolled back on failure, but consumed work is still charged. Consensus write conflicts are different. If the prepared transaction cannot commit because another transaction won the same write conflict, the prepared charge is dropped with the rest of the transaction. A retry runs the contract again and, if that retry commits, that retry is charged independently. Practical rule for app developers: handle contract-level failures as billable attempts; handle retry loops carefully so your app does not submit the same expensive work more than intended. # Storage Namespaces Source: https://docs.terminal3.io/t3n/how-t3n-works/z-namespace Tenant-owned TEE contracts and data are named in z-namespace ## Overview T3N is a confidential-computing network. TEE contracts and data run inside hardware-secured TEE nodes. Data are stored in a shared key-value (KV) store, and that store is divided into namespaces so tenant data and T3N data stay isolated. A namespace is a prefix on a map name. T3N uses that prefix to route each KV operation through the right access policy. There are two main worlds of namespaces: * **Tenant-owned namespaces (`z:`)** — open-ended. Tenants create maps and TEE contracts at runtime (i.e., register new maps and contracts while the cluster is running; do not need to be built into the T3N node ahead of time), and access is governed dynamically by tenant identity, contract identity, and map policy. * **T3N-owned namespaces (everything else)** — fixed. These are system maps such as `users`, `auth`, `dids`, `scripts`, `outbox_*`, and`idx:_tenant_*`, with strict built-in rules. ## Canonical format As an external developer, your TEE contracts and data belongs in z-namespace. Every z-namespace resource uses this canonical name: `z::` | Part | Meaning | Example | | -------- | ------------------------------------------------- | ------------------- | | `z:` | Marks a tenant-owned resource | `z:` | | `` | 40-hex suffix of your tenant DID, `did:t3n:` | `8f3a...c91d` | | `` | Tenant-local map or contract name | `secrets, bookings` | Example: `z:8f3a0123456789abcdef0123456789abcdefc91d:secrets` ``` All KV maps can be categorized as either: |-- System maps | |-- users, auth, dids, scripts, outbox_* | `-- idx:_tenants, idx:_tenant_contracts, idx:_tenant_maps | `-- Tenant maps |-- z::secrets |-- z::bookings |-- z::public:profile `-- z::secrets ``` Use the full canonical name when documenting, debugging, or reading audit output. High-level SDK helpers may ask only for the tenant-local tail. ## Access model T3N enforces tenant-map policy in the governed transaction path before writes commit. By default, a tenant contract can access only maps allowed by that map's policy: * Writers controls which **tenant contracts** may write. * Readers controls which **tenant contracts** may read. * If readers is omitted on map creation, it defaults from writers. * Cross-tenant access is denied unless the map owner explicitly grants another tenant contract access. This means cross-tenant access is possible by deliberate policy, but not by accident. **`writers`/`readers` gate your contracts, not you.** They confine which *contracts* may touch a map at runtime — your own contracts included: a contract you deploy still needs to be on `writers` to write. They do **not** restrict the map's owner. As the tenant that owns a map, you can always write or delete its entries directly through the authenticated control plane — e.g. `tenant.executeControl("map-entry-set", …)` — regardless of a `writers: { only: [...] }` grant, or add your contract to that grant via `tenant.maps.update` if you'd rather it write directly. (This is exactly how [seeding an API key](/developers/adk/tips/seed-api-key) writes into a contract-only `secrets` map.) So a "contract-only" map is **not tamper-proof against its own owner** — the ACL is a boundary between *your contracts* (and other tenants), not an integrity guarantee against you. If you need an audit trail that even the owner cannot silently rewrite, the map ACL is the wrong tool; that calls for a host-stamped, append-only primitive — see [Read your organization's activity log](/developers/adk/tips/get-activity-log) for the SDK's `getActivityLog()`, which exposes exactly that. ## Public tenant maps A tenant-public map must use both: ``` z::public: visibility = Public ``` Public maps are for data that is safe to expose. Never store PII, secrets, API keys, session data, or user-authentication material in a public map. Public visibility does not automatically let another tenant contract read through the authenticated contract path. Cross-tenant contract reads still require an explicit grant. ## User PII Tenant contracts cannot directly read the T3N users system map. Treat user profile data as outside z-space. If a product flow needs user profile fields, use the approved host capability path for that cluster. Do not copy PII into tenant maps just to make it easier for a contract to read. ## Rules of thumb * Use `z::` for canonical tenant names. * Use SDK tail inputs when the SDK asks for a tenant-local name. * Keep secrets and private contract state in private maps. * Put public data only under `z::public:`. * Set readers and writers deliberately. * Use explicit grants for intentional cross-tenant contract access. * Never put PII in public maps. * Do not expect tenant contracts to read platform maps directly. # What is the T3 Network (T3N)? Source: https://docs.terminal3.io/t3n/overview/what-is-t3n A decentralized confidential computing network T3 Network (T3N) is a **confidential computing network**. It is a cluster of nodes running inside hardware Trusted Execution Environments (TEEs) that **store and process private data confidentially, verifiably, and operator-blind**. Clients can prove their data reached genuine TEE hardware and was processed by audited code, and no single party (including node operators) can read or alter it. T3N addresses critical challenges in AI agent identity, permission and delegation, data privacy, and auditability challenges, making it suitable for high-stakes and inter-enterprise workflows. Example use cases include: * [**Delegate access to AI agents**](/t3n/use-cases/delegate-access-to-agent): enable AI agents to conduct "last mile” transactions (e.g., booking a hotel) without sharing sensitive data with the agents. * [**Delegate access to humans**](/t3n/use-cases/delegate-access-to-human): temporary and restricted access to a person's private data or credentials to trusted human helpers such as personal assistants, family members, or professionals, to complete certain tasks. * [**Confidential Multi-Party Computation (MPC)**](/t3n/use-cases/mpc): enable multiple parties to compute on joint datasets without sharing their sensitive data with each other or relying on a centralized, trusted third party. * [**Reusable verified user data**](/t3n/use-cases/reusable-user-data): reusable, interoperable, and regulatory compliant KYC/AML credentials that reduce friction, speed up onboarding, and keep data secure and private. ## Users The participants in the T3N ecosystem include: * **Developers** - Principals (individuals, organizations, or software agents) that build, integrate, or maintain AI agent applications. * **Data owners** - Principals that own or are legally responsible for private user or enterprise data and define how that data may be used. * **Data providers** - Principals that supply data to the network. Data providers are typically data owners, but may also be third parties or agents explicitly authorized by data owners. * **Data consumers** - Principals that access and use data from the network in accordance with granted permissions and applicable compensation terms. * **Node operators** - Principals responsible for operating, maintaining, and securing the infrastructure of the decentralized network. * **Compliance authorities** - Principals responsible for enforcing compliance with applicable laws, regulations, and industry standards. This includes government regulators and recognized industry bodies. * **Verifiable Credential (VC) issuers** - Principals that issue verifiable credentials that can be independently validated. * **VC verifiers** - Principals that verify the authenticity, integrity, and validity of verifiable credentials. ## Getting started Integrate your AI agent or application with T3N tools. # Why T3N? Source: https://docs.terminal3.io/t3n/overview/why-t3n Regain trust, privacy, and accountability in the age of AI ## What problems does T3N solve? * **Privacy risks:** Today, if you want an AI agent or human assistant to do meaningful work for you, you often have to hand over private data—such as personal identifiable information (PII), passwords, API keys, or private documents. This reliance creates significant privacy vulnerabilities. For examples, * AI-powered customer support / service (cx) agents could inadvertently disclose sensitive customer data, including account details, purchase histories, and financial balances. * Transaction AI agents pose a risk of leaking user payment information or executing unauthorized transactions. * **Lack of accountability:** AI agents are computer programs that act on behalf of humans. When AI agents misbehave, their owners need to be held responsible. For examples, * Autonomous trading agents execute unauthorized transactions, allowing owners to claim the agent acted without consent. * Procurement agents bypass approved policies, leading to disputes over authorization and liability. * **Lack of trust in the digital age:** AI has made creating digital identities cheap and effortless, but enterprises and users lack efficient, cost-effective ways to verify whether those identities are real and trustworthy. For examples, * Job seekers create fake resumes and credentials, and even attend online interviews with AI. * Malicious actors impersonate companies to phish data from job seekers. * You frequently have to re‑submit the same evidence (for example, that your identity was verified) instead of re‑using a credential that says it has already been checked. ## Why use T3N? Traditionally, platforms that store and process private data require you to blindly *trust the operator* — trusting that they won't inspect or sell your data, that they will prevent breaches, and that they actually run the code they claim to. This trust is unverifiable and frequently compromised. T3N shifts this paradigm. As a decentralized confidential computing network, T3N replaces operator trust with cryptographic verification. It is designed to help: **Developers** * Accelerate time-to-market by using pre-built infrastructure for secure data handling, confidential computing, and agent governance. * Build high-trust applications where you process private data without ever needing to see or store it yourself. * Tap into a global network of users with portable, verifiable credentials that work instantly with your service. **Data Owners** * Keep private data under your control, and let AI agents assist you without ever exposing your private information to them. * Maintain clear accountability for your AI agents, using cryptographic proofs to verify that every action was authorized by you. * Manage your identity and user data with secure, reusable verifiable, credentials. * Have the option to monetize your data yourself instead of it being fully monetized by a select number of companies. **Compliance authorities** * Enable AI agents to operate at their full potential while adhering to international data protection regulations, including but not limited to the EU’s General Data Protection Regulation (GDPR), California Consumer Privacy Act, and Singapore’s Personal Data Protection Act. **Data consumers** * Access a reliable, decentralized source of verified data to improve service accuracy and reduce the risk of fraud. * Minimize data acquisition overhead with predictable costs and direct access to high-quality, user-authorized information. **Verifiable credential (VC) issuers** * Monetize credential issuance by participating in a decentralized marketplace where high-value, verified data is rewarded. * Reduce administrative overhead by leveraging a standardized infrastructure for issuing and managing the lifecycle of digital credentials. **VC verifiers** * Ensure data authenticity with instant, cryptographic proof that a credential was issued by a trusted entity and has not been altered. * Improve user experience by enabling friction-less, near-instant verification that allows users to access services without lengthy waiting periods. * Reduce operational costs by automating the verification process and eliminating the need for expensive, manual background checks or repetitive third-party service fees. **Node operators** * Benefit from a growing ecosystem where increased network activity and data utilization translate into economic incentives. # Delegate Access to AI Agents Source: https://docs.terminal3.io/t3n/use-cases/delegate-access-to-agent How T3N allows you to delegate access to AI agents ## Enterprise Enterprises are significantly increasing their investments in AI agents, integrating these solutions into their core business and organizational processes. However, despite the enthusiasm, enterprises face major hurdles and have a strong need for robust solutions to address identity, permission and delegation, data privacy, and auditability challenges, particularly for high-stakes and inter-enterprise applications. ### B2B Procurement B2B procurement today is burdened by fragmented systems, manual handoffs, and high trust and compliance overhead across enterprises. While core processes such as supplier selection, purchase order creation, fulfillment confirmation, invoicing, and payment are well defined, they remain slow, error-prone, and difficult to automate safely due to data-sharing risks, approval constraints, and audit requirements. An AI agent–driven procurement flow on T3N addresses these challenges by enabling enterprise AI agents to transact on behalf of their organizations under explicit, policy-bound delegation, with secure data custody, verifiable inter-agent communication, and auditable execution across buyer and supplier systems. High level AI agent–driven B2B procurement architecture (Light mode) High level AI agent–driven B2B procurement architecture (Dark mode) A typical AI agent–driven B2B procurement transaction flow between two enterprises AI agents on T3N can look like this: 1. Enterprise buyer (i.e., data owner) stores its payment info (e.g., enterprise account, stablecoin wallet, or payment gateway key) once in T3N. 2. The Enterprise buyer authorizes a Buyer AI Agent to procure goods or services within predefined constraints (e.g., approved suppliers, SKU list, pricing bands, delivery terms, and spending limits). 3. The Buyer AI Agent prepares a purchase order (PO) via the enterprise ERP system using pre-agreed commercial terms. 4. The Buyer AI Agent sends the PO to the approved Supplier AI Agent via T3N. 5. The Supplier AI Agent receives the PO, validates it, and confirms acceptance, delivery schedule, and fulfillment details back to the Buyer AI Agent via T3N. 6. Upon delivery, the Buyer AI Agent confirms receipt, and the Supplier AI Agent issues an invoice through its ERP system. 7. Before payment execution, the Buyer AI Agent again verifies that the supplier remains approved. 8. For the final payment step, instead of accessing payment keys directly, the Buyer AI Agent submits a payment instruction to T3N, which securely delivers the pre-configured payment info to the payment platform or enterprise system. 9. The payment platform executes the transaction, and T3N processes and sanitizes the response before returning a confirmation to the Buyer AI Agent and Supplier AI Agent, ensuring sensitive financial data is never exposed to either agent. ### Payroll Payroll today is constrained by sensitive employee data, fragmented HRIS and payroll systems, approval workflows, banking integrations, tax reporting requirements, and strict audit obligations. While recurring payroll processes such as timesheet validation, compensation calculation, deduction handling, benefits reconciliation, tax withholding, and payment execution are well defined, they remain difficult to automate safely because AI agents must not be given direct access to employee PII, bank account details, payroll provider credentials, or treasury payment keys. An AI agent–driven payroll flow on T3N addresses these challenges by enabling enterprise AI agents to execute payroll tasks under explicit, policy-bound delegation, with secure custody of sensitive payroll data, verifiable approvals, and auditable execution across HR, payroll, banking, benefits, and tax systems. A typical AI agent–driven payroll transaction flow within an enterprise on T3N can look like this: 1. The enterprise payroll team (i.e., data owner) stores sensitive payroll execution data once in T3N, such as payroll provider credentials, treasury payment authorization keys, employer tax account information, and approved payroll policy constraints. 2. Employees or HR systems store required employee payroll data, such as legal identity details, tax forms, benefits elections, direct deposit information, and compensation records, with access governed by explicit policies. 3. The enterprise authorizes a Payroll AI Agent to prepare and run payroll within predefined constraints, such as eligible employee groups, payroll calendar, approval thresholds, compensation rules, benefit deductions, tax jurisdictions, and funding limits. 4. The Payroll AI Agent retrieves non-sensitive payroll inputs from the HRIS, time tracking system, benefits platform, and finance system, then calculates gross pay, deductions, reimbursements, taxes, and net pay. 5. The Payroll AI Agent submits the prepared payroll run for required human or system approvals through the enterprise workflow system. 6. After approval, the Payroll AI Agent validates that the payroll run still satisfies the delegated policy constraints, including employee eligibility, payment totals, approval status, and funding limits. 7. For the final payroll execution step, instead of accessing employee bank details, payroll provider credentials, or treasury payment keys directly, the Payroll AI Agent submits a payroll execution instruction to T3N. 8. T3N securely delivers the required sensitive payroll data directly to the payroll provider, banking platform, benefits administrator, or tax system needed to complete the transaction. 9. The payroll provider and connected systems execute salary payments, benefit contributions, tax withholdings, and required filings. 10. T3N processes and sanitizes the execution responses before returning payroll status, exception details, and audit records to the Payroll AI Agent, ensuring sensitive employee and financial data is never exposed to the agent. ## Individual AI agents are rapidly becoming essential tools for individuals automating complex, multi-step tasks that traditionally required human intervention, such as booking travel or managing financial portfolios. However, for these agents to complete "last-mile" transactions (e.g., confirming a flight booking with a payment), they often require access to highly sensitive user data, like payment details, passport numbers, credential, or API keys. Current systems often require users to grant agents full access to this data, creating significant privacy and security risks. If the agent or the platform it resides on is compromised, the sensitive data is exposed, as seen in the [UnitedHealth Group](https://www.reuters.com/business/healthcare-pharmaceuticals/unitedhealth-confirms-190-million-americans-affected-by-hack-tech-unit-2025-01-24/) and [Middle East call center](https://www.resecurity.com/blog/article/cybercriminals-are-targeting-ai-agents-and-conversational-platforms-emerging-risks-for-businesses-and-consumers) breaches. A typical AI agent-driven transaction flow for individuals on T3N can look like this: 1. You (i.e., data owner) store your private user data once in T3N. 2. You authorize an AI agent (e.g., a travel booking agent) to perform a specific action (e.g., "Book me a flight to Dubai using my preferred credit card"). 3. The AI agent processes the request (e.g., finding the cheapest direct flight). 4. When the AI agent is about to initiate the last-mile transaction (e.g., actual booking), instead of exposing the private user data to the agent, the AI agent interacts with T3N, which then securely delivers the required private user data directly to the third party (e.g., the airline’s booking system). 5. The third party (e.g., the airline’s booking system) receives the data, completes the transaction. 6. T3N processes the third-party response (e.g., from the airline’s booking system) before forwarding it, ensuring any private user data is processed first and not returned to the AI agent. Examples of automating transactional tasks with AI agents for individuals: * Online financial product applications (credit cards, personal loans). * Insurance claim management. * Customer support interactions. * Job searching tasks. * Patient registration. * Online shopping. # Delegate Access to Human Helpers Source: https://docs.terminal3.io/t3n/use-cases/delegate-access-to-human How T3N allows you to delegate access to human helpers While AI agents excel at automation, a trusted agent might not always be available for every task. Therefore, temporary and restricted access to a person's private data or credentials may still be necessary for human helpers, such as personal assistants, family members, or professionals, to complete certain tasks. Current methods, however, often involve sharing the data via email or a chat app (e.g., WhatsApp), which is inherently insecure and risks over-exposure of sensitive information. A typical flow for delegating access to a human helper on T3N can look like this: 1. You (i.e., data owner) store your private user data once in T3N. 2. You authorize a human helper (e.g., a family member) to access necessary private user data to perform a specific action (e.g., "Apply for Thailand Digital Arrival Card") based on your configurable rules (e.g., single or multiple uses, expiration dates). 3. The human helper processes the request (e.g., filling Thailand Digital Arrival Card application form). 4. When the human helper requires your private user data, they interact with T3N to access the authorized data (e.g., passport information). 5. The third party (e.g., Thailand Immigration Bureau) receives the data, completes the transaction. 6. T3N automatically revokes access based on the authorization rules. # Confidential Multi-Party Computation Source: https://docs.terminal3.io/t3n/use-cases/mpc How T3N enables confidential multiparty computation (MPC) Currently, multiple parties that wish to collaborate or compute on joint datasets (e.g., analyzing overlapping customer bases) must either share their sensitive data with each other or rely on a centralized, trusted third party, both of which risk exposing private information. For instance, two competing banks attempting to identify shared fraudulent activity cannot legally share their customers' Personally Identifiable Information (PII) with one another to run a joint analysis. A typical confidential multiparty computation flow on T3N can look like this: 1. Multiple data owners (e.g., Bank A and Bank B) store their private data in T3N or allow T3N to access their private data. 2. The parties authorize a mutually agreed-upon TEE contract that defines the exact computation to run on the combined dataset. 3. When the computation executes, T3N securely processes the combined data inside a hardware Trusted Execution Environment (TEE), ensuring the raw inputs remain hidden from all parties. 4. The TEE contract outputs only the final computed results to the authorized parties, keeping the underlying data completely private. Examples of confidential multiparty computation: * Collaborative fraud detection among financial institutions. * Joint medical research across multiple healthcare providers. * Secure supply chain optimization between competing enterprises. * Privacy-preserving advertising attribution and audience matching. * Benchmarking and market analysis on aggregated proprietary datasets. # Reusable Verified User Data Source: https://docs.terminal3.io/t3n/use-cases/reusable-user-data How T3N allows you to reuse verified user data Currently, data owners must repeatedly provide verified data (e.g., KYC info) whenever they interact with different third parties, both in physical and digital settings (e.g., banks or telecom service providers). For instance, a user opening an account at Bank A must complete a KYC verification. If that user then engages with Bank B, a similar time-consuming and costly KYC process must be repeated. A typical reusable verified user data flow on T3N can look like this: 1. You (i.e., data owner) store your private user data once in T3N. 2. A trusted issuer, a reputable and verifiable organization, issues you a verifiable credential that attests to specific user claims within the credential. 3. When a relying third party needs to verify you, you share that credential rather than uploading raw documents again. Examples of reusable verified user data: * Reusable KYC information. * Proof of accredited investor qualification. * Academic degrees and professional licenses. * Loyalty program membership. * Vaccination records.