Skip to content
Adeia

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.

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.

  1. Start the layer

    $ npm install
    $ npm run seed
    $ npm run dev

    seed prints one API key and never prints it again — only its hash is stored. The boot banner names the adapter it registered.

  2. Add the client

    $ npm install @adeia/sdk

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

  3. Point it somewhere

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

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

The sixty-second version

The smallest thing that works

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.

import { AdeiaClient } from "@adeia/sdk";

const adeia = new AdeiaClient({
  apiKey: process.env.ADEIA_API_KEY!,
  baseUrl: process.env.ADEIA_URL,        // default http://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
}

Reference

Every method, and when it throws

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

What every method resolves to

interface ActionRecord {
  id: string;                 // act_…
  projectId: string;
  type: string;               // "payment" is the only type today
  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 that decided it
  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.

Constructor

new AdeiaClient(options)

new AdeiaClient(options: AdeiaClientOptions): AdeiaClient
apiKey required
The key npm run seed printed once. Sent as authorization: Bearer <key> on every request.
baseUrl http://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 ErrorAdeiaClient 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.
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.
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.

BACK TO THE OVERVIEW