Build your first agent
A forty-line TypeScript script that:
- Polls the agent's inbox.
- Asks an LLM for a reply.
- Sends that reply back out through Inbox.
- 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_KEYis scoped toagent-hello. It cannot readagent-triage's mail or writeagent-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.