Headless agent.
What you're building
A server-side agent with no UI: it reads the user's memory on a loop, does work, and writes results back. The payoff is automation that runs unattended — no chat surface, no front end — while still building on the same portable memory every other PAMbase app shares.
The connect flow still happens once: the user authorizes in the hub, you receive a short-lived code, and you exchange it for a long-lived connection token stored server-side. You hold an OAuth client id and secret, and the read/write scopes (one permission each) your agent needs.
Scopes to request
| Scope | Why |
|---|---|
memory:read:* | read whatever the agent reasons over |
memory:write:general | persist results and observations |
Narrow these to the specific namespaces your agent uses. See permissions.
Connect once, then run headless
Even with no UI, exchange the authorization code for a connection_token after the user authorizes in the hub. The code is single-use and expires in 10 minutes; the token lasts about a year.
import { exchangeCodeForToken } from "@pambase/sdk";const { connection_token, ai_id, scopes } = await exchangeCodeForToken({apiBaseUrl: process.env.PAMBASE_API_URL!,code: req.query.code, // single-use, expires in 10 minclientId: process.env.PAMBASE_CLIENT_ID!,clientSecret: process.env.PAMBASE_CLIENT_SECRET!,});// → { connection_token: "ct_…", ai_id: "ai_…", scopes: ["memory:read:*", "memory:write:general"] }await vault.put(`pambase:${ai_id}`, connection_token); // store securely — never in code or logs
What to write
Persist results as durable memory — plain natural-language content in the scope you name. No LLM required to write.
await pambase.remember({content: "Flagged 3 stale invoices for review.",kind: "event",scope: "general",});// res → { accepted: true, memoryIds: ["mem_…"] }
How to read
Pure reads: recall for ranked, relevant memories, or getContext for a selected bundle.
const { memories } = await pambase.recall({ query: "open tasks", limit: 20 });// memories → [{ id, type, scope, content, importance, occurredAt, sourceApp? }, …]
Putting it together — a cron loop
Run one tick per connected user on a schedule. Read, act, write, and queue the next run. If a call returns 401 the connection token was revoked — there is no refresh, so you must re-run the connect flow.
import { PAMbaseApp } from "@pambase/sdk";async function tick(aiId: string) {const token = await vault.get(`pambase:${aiId}`);const pambase = new PAMbaseApp({ baseUrl: process.env.PAMBASE_API_URL!, connectionToken: token });try {const { memories } = await pambase.recall({ query: "open tasks", limit: 20 });const result = await doWork(memories);await pambase.remember({ content: result, kind: "event", scope: "general" });await pambase.schedule({ fireAt: nextRunISO(), kind: "tick" }); // → { id, fireAt }} catch (e) {if (e.status === 401) await markReauthNeeded(aiId); // token revoked → re-run connect flow}}// cron: every hour, for each connected usersetInterval(() => connectedAiIds().forEach(tick), 60 * 60_000);
The connection_token is long-lived and grants full access for that user — keep it in a secrets vault, never in code or logs (see security). There is no refresh token: on a 401, stop the loop and re-run the connect flow for a fresh token.
Next
- Connect flow — the one-time authorization and token exchange.
- Security — storing the connection token safely.
- Request context — the bundle option for reads.