Skip to content
adeia fence
Sign in
Reference · @adeia/sdk

The client
your agent calls

One call asks for an action and gets back one that has already been decided — run, held for a person, or refused with the figure that refused it. Every method, option and error on this page exists in src/sdk/src/index.ts; nothing here is a plan.

3 methods
2 error classes
0 automatic retries
0 HTTP dependencies
Install

What a builder types

Node 22 or newer, ESM. The client is plain fetch with no HTTP dependency, and it reads node:crypto for idempotency keys — so it runs on your server, not in a browser.

01

Start the layer

The boot banner names the adapters it registered — and says NO PAYMENT PROCESSOR ATTACHED, because none is.

shell
$ npm install
$ npm run dev
02

Get a key

Sign in with GitHub and issue one from the dashboard. It is shown once and never again — only its hash is stored. npm run seed does the same thing from the command line if you would rather not open a browser.

shell
$ npm run seed
adeia_sk_…  copy it now; it is not printed again
03

Add the client

Not on the public registry yet. Inside this repository it resolves through npm workspaces, so the root npm install is already enough.

shell
$ npm install @adeia/sdk
04

Point it somewhere

The URL is optional — leave it out and the client talks to http://localhost:3000.

.env
ADEIA_API_KEY=adeia_sk_…
ADEIA_URL=http://localhost:3000

The sixty-second version

Two calls. The first always resolves, whatever the policy decided. The second is only reached when a person has to be asked, and it blocks until they answer or the deadline passes.

Money is integer cents the whole way through. If your model talks in dollars, round at that boundary and nowhere else — 0.29 * 100 is not 29.

agent.ts
import { AdeiaClient } from "@adeia/sdk";

const adeia = new AdeiaClient({
  apiKey: process.env.ADEIA_API_KEY!,
  baseUrl: process.env.ADEIA_URL,  // default localhost:3000
});

const action = await adeia.requestAction({
  type: "payment",
  params: {
    amountCents: 2500,          // integer cents, never a float
    currency: "usd",
    recipient: "acct_cloudhost",
    description: "monthly hosting",
  },
});

if (action.status === "pending_approval") {
  const final = await adeia.waitForAction(action.id);
  console.log(final.status);  // executed | denied | expired | failed
}
The record

What every method resolves to

Three methods and a constructor. Everything resolves to the same record, so the shape below is worth reading once before the rest.

ActionRecord
interface ActionRecord {
  id: string;          // act_…
  projectId: string;
  type: string;        // "payment" or "http"
  params: Record<string, unknown>;
  status:
    | "pending_policy" | "pending_approval"
    | "approved"       | "executing"
    | "executed"       | "failed"
    | "denied"         | "expired";
  decision: "allow" | "require_approval" | "deny" | null;
  decisionReason: string | null;   // names the figure
  idempotencyKey: string;
  result: Record<string, unknown> | null;
  error: string | null;
  createdAt: string;   // ISO-8601 UTC
  decidedAt: string | null;
  executedAt: string | null;
}

The four terminal statuses are executed, failed, denied and expired. An action never leaves one, and waitForAction polls until it reaches one.

decisionReason is the field worth reading aloud. It is not a category — it carries the number that decided, so an agent can tell whoever asked why rather than that.

Reference

Every method, and when it throws

Constructor

new AdeiaClient(options)

new AdeiaClient(options: AdeiaClientOptions): AdeiaClient
apiKey required
The key the dashboard or npm run seed printed once. Sent as authorization: Bearer <key> on every request.
baseUrl localhost:3000
Where the layer is listening. Trailing slashes are trimmed, so a URL out of an env file does not produce a double slash.
fetch global fetch
Injectable purely so tests need no live server. Anything matching the fetch signature will do.
Throws
A plain Error — AdeiaClient requires an apiKey — when the key is empty. Construction is synchronous and sends nothing; the first request happens when you call a method.

Method

requestAction(req)

requestAction(req: ActionRequestInput): Promise<ActionRecord>
Returns
The decided record. Executed, held for a person, or refused — the promise resolves either way, so read status rather than assuming success.
Throws
AdeiaError on any non-2xx response: invalid_request (400, carrying issues), unauthorized (401), internal_error (500). A policy denial is none of these.
example
await adeia.requestAction({
  type: "payment",
  idempotencyKey: `invoice-${invoice.id}`,  // optional
  params: {
    amountCents: 50000,               // integer, positive
    currency: "usd",                  // three lowercase letters
    recipient: "acct_contractor",
    description: "Q3 design work",    // optional, ≤ 500 chars
  },
});

Omit idempotencyKey and the client generates a fresh UUID for that call. Supply your own when a retry should be recognised as the same logical action — a new key is a new payment. The body is validated strictly, so a misspelled field is a 400 you can see rather than a key that vanishes.

There is no automatic retry, deliberately. A blind retry against a payment endpoint is how double charges happen. The idempotency key makes a retry safe; the decision to make one stays with you.

Method

getAction(id)

getAction(id: string): Promise<ActionRecord>
Returns
The record as it stands right now. The id is URL-encoded for you.
Throws
AdeiaError with status: 404 and code: "not_found" for an id that does not exist — and for another project's action, which is deliberately the same answer. A 403 there would confirm the id exists.

Method

waitForAction(id, opts?)

waitForAction(id: string, opts?: WaitOptions): Promise<ActionRecord>
timeoutMs 300000
Five minutes. Long enough for someone to read an approval email and click through, short enough that an ignored request does not hang an agent forever.
pollMs 2000
How long it sleeps between reads.
Returns
The record once it reaches a terminal status. It reads once before it starts waiting, so an action that has already finished comes back without a sleep.
Throws
AdeiaTimeoutError when the deadline passes while the action is still in flight. That is not a failed action — a human may simply not have decided yet, and the id is still good. Any AdeiaError from the underlying read passes straight through.

Error

AdeiaError

class AdeiaError extends Error {
  name: "AdeiaError";
  status: number;
  code: string;
  issues?: unknown[];
}
status
The HTTP status of the response.
code
The server's machine-readable code — one of unauthorized, not_found, invalid_request, internal_error — so you can branch without parsing prose. It falls back to unknown_error when a response carried none.
issues optional
The validation issue list, present only when the server sent one. This is where a bad amount or a missing recipient shows up.
example
import { AdeiaError } from "@adeia/sdk";

try {
  await adeia.requestAction(req);
} catch (err) {
  if (err instanceof AdeiaError && err.code === "invalid_request") {
    console.error(err.issues);
  }
}

The message reads POST /v1/actions failed with 400. The detail is on the properties, not in the prose.

Error

AdeiaTimeoutError

class AdeiaTimeoutError extends Error {
  name: "AdeiaTimeoutError";
  actionId: string;
  lastStatus: string;
}
Thrown by
waitForAction, and nothing else.
Carries
actionId and lastStatus, so you can report what it was still doing, or come back and poll the same id later.
It extends Error directly, not AdeiaError. A check for err instanceof AdeiaError is false for a timeout — branch on it by name.
Outcomes

Three answers, three jobs

One call, three things it can come back as. They are not interchangeable, and the status code each one carries is part of the answer.

executed

HTTP 201

Inside the fence. The policy answered allow, the adapter ran, and result holds what it wrote.

What to do Nothing further. Read result and carry on. Note the payment result says settled: false — no processor is attached, so the action was authorised and recorded, not paid.

pending_approval

HTTP 202

Over a limit. The action stopped where it stood, an email went to a named human, and nothing downstream has been contacted. decisionReason says which rule tripped.

What to do Call waitForAction(action.id). It resolves executed, denied or expired, or throws AdeiaTimeoutError if nobody has answered in five minutes. 202 is the one code that means not finished — poll this.

denied

HTTP 200

Refused by policy. Past the hard ceiling or over the daily cap, no approval request is sent and no button anywhere lets a tired human wave it through.

What to do Read decisionReason, say it out loud to whoever asked, and stop. Do not retry — the same request will get the same answer every time.

The one that bites: a denial is a 200, not an error. The call succeeded. The request was well formed, it was received, it was evaluated, and the answer was no — that is a result, not a transport failure. It resolves as an ordinary record and never reaches your catch block. A client that treats it as a failed request will retry a refusal that cannot change, at whatever interval its backoff decides, forever. That is the single most likely way to misuse this SDK, which is why requestAction does not retry anything on your behalf.

Two more terminal statuses exist and neither throws either. failed (201) is an action that reached its adapter and the adapter refused it — error carries the reason, and a 500 there would make callers retry something that may already have landed. expired is an approval nobody answered before the token lapsed.

In closing

It asks. It never decides.

The client is small on purpose. Everything it is careful not to do is the reason the thing on the other side can be trusted with an agent.

  • It holds no limits Every figure lives server-side in a policy the agent cannot read or edit. Nothing you pass to this client can widen the fence.
  • It reaches no processor The agent only ever sees this client. What executes on the far side of the seam is not something the model can name, let alone call.
  • It retries nothing No backoff, no silent second attempt. The idempotency key makes a retry safe when you choose to make one.