Skip to main content
The OpenAI Agents SDK is OpenAI’s lightweight framework for building agents in TypeScript and Python. It ships the parts that run an agent:
  • The agent loop — call the model, run tools, feed results back, repeat until done.
  • Handoffs — route a task to a specialized agent (triage → spend → reporting).
  • Guardrails — validate inputs/outputs and bail early on policy violations.
  • Sessions, tracing, and human-in-the-loop — pause a run for approval and resume it.
What it doesn’t ship is a way for those agents to act as a real account — sign up for a SaaS tool, hold a funded card, or spend within a budget you control, per end-user. That’s the half Naive adds. You keep the SDK’s orchestration; Naive gives each agent a tenant identity, a virtual card, 1,000+ third-party connections, and an Account Kit that bounds exactly what the agent can do — enforced server-side, with human approval on the sensitive actions.

How the pairing works

  • Naive’s SDK exposes a small, drop-in toolset via agentTools() — a discover-then-run meta-toolset (search apps/primitives, then run them) instead of thousands of schemas.
  • Each Naive tool ships an Anthropic-style input_schema (plain JSON Schema), and the OpenAI Agents SDK’s tool() helper accepts a raw JSON schema directly — so the adapter is a few lines.
  • Every call stays gated by the user’s Account Kit on Naive’s servers; sensitive actions resolve to a pending_approval payload.
  OpenAI Agents SDK (run)               Naive
  ─────────────────────                 ─────
  agent ── tool call ──▶ loop
        │  model picks a tool

  tool.execute(input)  ──────────▶  kit.handle(name, input)
                                          │  AccountKit-gated, scoped to one user

                                    connect Gmail · issue a $50 card · run a capability

                                    sensitive? → 202 pending_approval (human-in-the-loop)
Tested against: @usenaive-sdk/server 0.7.x (Naive API v2), @openai/agents 0.11.x (Agent, run, tool, MCPServerSSE), on Node ≥ 20.tool() accepts a raw JSON schema for parameters; this guide passes Naive’s input_schema through directly and sets strict: false, because the discover-then-run tools use optional fields that aren’t strict-mode JSON Schema. Naive re-validates every call server-side regardless. For the MCP extension this guide uses MCPServerSSE (Naive’s hosted MCP server speaks SSE). Pin your versions and set model to a model you have access to.

Prerequisites

  • A Naive API key (nv_sk_...) — get one from the dashboard.
  • An OPENAI_API_KEY for the model that runs the agent (the SDK reads it by default).
  • Node ≥ 20.
npm install @usenaive-sdk/server @openai/agents
export NAIVE_API_KEY=nv_sk_live_...
export OPENAI_API_KEY=sk-...

Minimal viable integration

The shortest path to an OpenAI Agents SDK agent that can actually transact: define a policy, provision a user, adapt Naive’s tools, and run the agent.
1

Define the policy, then provision a user

An Account Kit is the spend/capability policy. Here a tenant user gets a card (capped at $500, approval required), the vault, and an allowlist of apps. Everything the agent does is bounded by this kit — server-side.
import { Naive } from "@usenaive-sdk/server";

const naive = new Naive({ apiKey: process.env.NAIVE_API_KEY! });

// Control plane: a reusable policy template.
const pro = await naive.accountKits.create({
  name: "Pro",
  primitives_config: {
    cards: { enabled: true, requiresApproval: true, defaults: { spending_limit_cents: 50000 } },
    vault: { enabled: true },
  },
  connections_config: { mode: "allowlist", toolkits: ["github", "gmail", "stripe"] },
});

// Provision one of your end-users and assign the kit.
const alice = await naive.users.create({
  external_id: "user_123",
  email: "alice@acme.com",
  account_kit_id: pro.id,
});

// Data plane: a client bound to Alice. No call here takes a userId — the scope is fixed.
const client = naive.forUser(alice.id);
2

Adapt Naive's tools into OpenAI Agents tools

client.agentTools() returns tools as JSON-schema definitions plus a handle(name, input) dispatcher. The SDK’s tool() takes a raw JSON schema in parameters, so the adapter maps each Naive tool’s input_schema and routes the executor through handle:
import { tool } from "@openai/agents";
import type { NaiveAgentToolset } from "@usenaive-sdk/server";

function naiveToolsForOpenAI(kit: NaiveAgentToolset) {
  return kit.tools.map((t) =>
    tool({
      name: t.name,
      description: t.description,
      parameters: t.input_schema, // Anthropic-style JSON Schema — passed through as-is
      strict: false,              // discover-then-run tools use optional fields
      execute: (input) => kit.handle(t.name, input as Record<string, unknown>),
    }),
  );
}
This yields the discover-then-run meta-tools (naive_search_apps, naive_connect_app, naive_run_capability, naive_search_primitives, naive_run_primitive, …) — a handful of tools that reach every app and primitive the kit allows, instead of thousands of schemas.
With strict: false the SDK passes arguments through without local schema validation, but Naive re-validates every call server-side against the Account Kit — so the model can’t smuggle out-of-policy arguments past the agent.
3

Build the agent — and let it transact

Hand the adapted tools to an Agent, then run it. The SDK runs the multi-step loop for you (call tool → feed result back → continue):
import { Agent, run } from "@openai/agents";

const agent = new Agent({
  name: "Ops Agent",
  model: "gpt-5.1",
  instructions:
    "You are Alice's operations agent. Use Naive tools to act on her real account.",
  tools: naiveToolsForOpenAI(client.agentTools()),
});

const result = await run(
  agent,
  "Connect my GitHub, then issue a $50 virtual card called 'Ads budget' for our marketing spend.",
);

console.log(result.finalOutput);
  • The model discovers GitHub (naive_connect_app), returns a connect link for Alice to authorize, and attempts to issue the card (naive_run_primitivecards.create).
  • The card is a real card on Alice’s account, capped by her kit.
  • The whole agent loop is the SDK’s; the real-world actions are Naive’s.
That’s the moat in ~40 lines: the same Agent that would otherwise just describe spending money now issues a policy-bounded card on a specific user’s account.

Extension: human-in-the-loop spend (two gates)

Because the kit set cards.requiresApproval: true, the agent cannot silently spend. Pair the SDK’s native approval flow with Naive’s server-side gate for defense in depth:
  • In the run — mark the spend tool needsApproval. The SDK pauses the run and returns interruptions before that tool executes, so you surface the call for review.
  • On the server — even if a call gets through, Naive freezes it and returns a pending approval (HTTP 202) instead of a live card. This holds no matter what runtime calls it.
Gate the primitive-runner tool so the run pauses before it fires:
function naiveToolsForOpenAI(kit: NaiveAgentToolset) {
  return kit.tools.map((t) =>
    tool({
      name: t.name,
      description: t.description,
      parameters: t.input_schema,
      strict: false,
      // Pause the run before Naive's primitive-runner (cards, domains, KYC, …) executes.
      needsApproval: t.name === "naive_run_primitive",
      execute: (input) => kit.handle(t.name, input as Record<string, unknown>),
    }),
  );
}
Resolve interruptions out of band, then resume the run from the same state:
import { run } from "@openai/agents";

let result = await run(agent, "Issue a $50 virtual card called 'Ads budget'.");

while (result.interruptions?.length) {
  for (const interruption of result.interruptions) {
    // ...show interruption.name / interruption.arguments to a human in your UI...
    result.state.approve(interruption); // or: result.state.reject(interruption)
  }
  result = await run(agent, result.state); // resume from the paused snapshot
}

console.log(result.finalOutput);
When a call does reach Naive, the tool result comes back as a pending approval rather than a live card:
{
  "status": "pending_approval",
  "approval_id": "65589c8b-e033-4a65-b16c-379211c94429",
  "action": "cards.create",
  "primitive": "cards",
  "title": "Issue virtual card \"Ads budget\"",
  "message": "This action requires human approval before it executes."
}
The immediate 202 / isPendingApproval payload uses action; approval records from approvals.list() or approvals.get() use action_type. Your app then resolves it out of band — and on approval, Naive replays the frozen action server-side:
import { isPendingApproval } from "@usenaive-sdk/server";

// Find what the agent queued for Alice.
const { approvals } = await client.approvals.list({ status: "pending" });

for (const a of approvals) {
  // ...show a.title / a.action_type to a human in your UI...
  await client.approvals.approve(a.id); // API replays cards.create → real card
  // or: await client.approvals.deny(a.id, { reason: "over budget" });
}
You can also catch the pending state at the SDK call site with isPendingApproval(res), or poll a single approval to completion with client.approvals.wait(approvalId). See Approvals for the full lifecycle (pending → executed / failed / denied).
Approvals are only enforced for agent (API-key / MCP) calls on real tenant users. A human acting in your dashboard, and agent calls on the operator’s own default user, bypass the gate — so end-user agents stay governed while your own automation isn’t slowed down.

Alternative: hand a scoped MCP session to the agent

If the agent runs somewhere you don’t fully trust (an edge runtime, a third-party host), don’t ship it your API key. Mint a short-lived, per-user MCP session and point an MCPServerSSE at it — the bearer lives only in the request headers and expires. The SDK auto-discovers Naive’s tools as MCP tools:
import { Agent, run, MCPServerSSE } from "@openai/agents";

const session = await client.session({ ttlMs: 15 * 60 * 1000 });

const naiveMcp = new MCPServerSSE({
  name: "naive",
  url: session.mcp.url,                          // scoped, per-user endpoint
  cacheToolsList: true,
  requestInit: { headers: session.mcp.headers }, // scoped bearer — never in the URL
});

const agent = new Agent({
  name: "Ops Agent",
  model: "gpt-5.1",
  instructions: "Use the Naive tools to act on Alice's real account.",
  mcpServers: [naiveMcp],
});

try {
  await naiveMcp.connect();
  const result = await run(agent, "Issue a $50 virtual card called 'Ads budget'.");
  console.log(result.finalOutput);
} finally {
  await naiveMcp.close();
}
  • Same Account Kit, same approval gates — just delivered as a remote MCP server instead of in-process tools.
  • The session is scoped to one user and expires (default 15 min, max 24h); revoke early with client.sessions.revoke(session.id).

What stays enforced

No matter which path you choose, the policy is enforced where it matters — on Naive’s servers, not in your prompt or your agent config:
  • Identity — every action runs as a specific tenant user, fully isolated from your other users.
  • Capability bounds — the Account Kit decides which primitives and which apps the agent can touch (allowlist / blocklist / per-tool).
  • Scoped spend — virtual cards are capped per card and per user; the model can’t raise its own limit.
  • Human-in-the-loop — sensitive actions (cards, domains, KYC, formation, connecting an app) freeze as approvals until a human says yes.

Next steps

  • SDK overview — the full Naive client surface
  • Agent tools — what agentTools() exposes and how handle() works
  • Account Kits — author spend/capability policy
  • Approvals — the human-in-the-loop lifecycle
  • Sessions — per-user MCP sessions for untrusted runtimes