Skip to main content

Build your first agent

A forty-line TypeScript script that:

  1. Polls the agent's inbox.
  2. Asks an LLM for a reply.
  3. Sends that reply back out through Inbox.
  4. Writes a one-line summary to Provenance.

The code

first-agent.ts
import {UjexClient} from "@ujexdev/client";

const ujex = new UjexClient({
firebase: {
apiKey: process.env.UJEX_FIREBASE_API_KEY!,
authDomain: "axy-ujex.firebaseapp.com",
projectId: "axy-ujex",
appId: process.env.UJEX_FIREBASE_APP_ID!,
},
agentId: process.env.UJEX_AGENT_ID!,
deviceKey: process.env.UJEX_DEVICE_KEY!,
});

async function llm(subject: string, body: string): Promise<string> {
// Swap for your model of choice. Keep it boring here.
return `Re: ${subject}\n\nThanks for your note. I saw: "${body.slice(0, 80)}..."`;
}

async function tick() {
const { messages } = await ujex.postbox.listMessages({ direction: "inbound", limit: 5 });
for (const m of messages) {
if (!m.from) continue;
const subject = m.subject ?? "(no subject)";
const reply = await llm(subject, m.body ?? "");
await ujex.postbox.send({
to: [m.from],
subject: `Re: ${subject}`, body: reply,
sessionId: `first-agent-${m.id}`,
requireHuman: true,
});
await ujex.memory.write({
name: `reply-${m.id}`,
type: "episode",
content: `Replied to ${m.from} about "${subject}"`,
sessionId: `first-agent-${m.id}`,
});
}
}

await ujex.connect();
setInterval(tick, 15_000);

Run it

UJEX_AGENT_ID="agent-hello" \
UJEX_DEVICE_KEY="ap_live_..." \
UJEX_FIREBASE_API_KEY="AIza..." \
UJEX_FIREBASE_APP_ID="1:...:web:..." \
npx tsx first-agent.ts

What's good about this shape

  • No raw credentials in Inbox calls. The SDK exchanges the device key through session, signs in with Firebase, and then calls live callables with a short-lived bearer token.
  • No database coupling. The agent doesn't know about Cloud Scheduler, Firestore indexes, or rules. It speaks SDK calls.
  • The agent's identity is leaf-level. A leaked UJEX_DEVICE_KEY is scoped to agent-hello. It cannot read agent-triage's mail or write agent-triage's memory.

Where to go from here

  • Move from polling to signed inbound webhooks when your loop is ready.
  • Turn the llm() call into something real and gate its spend via Budgets.
  • Keep recurring work outside first-run until the core trust loop is boring.