NOIR
Get started

One-file agent

Build a funny Slack operator with OpenRouter, Stripe, Supermemory, a user-owned database, and a bring-your-own sandbox.

This example is intentionally complete enough to deploy while keeping the application surface in one file. The model comes directly from OpenRouter. Stripe supplies its own toolkit. The database and sandbox are ordinary modules in your repository.

agent.ts
import { noir } from '@noir-agent/agent'
import { slack } from '@noir-agent/agent/channels/slack'
import { supermemory } from '@noir-agent/agent/memory/supermemory'
import { stripe } from '@noir-agent/agent/plugins/stripe'
import { sandbox } from '@noir-agent/agent/sandbox'
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
import { createStripeAgentToolkit } from '@stripe/agent-toolkit/ai-sdk'
import { database } from './database.js'
import { sandboxDriver } from './sandbox.js'

const openrouter = createOpenRouter({
  apiKey: requiredEnv('OPENROUTER_API_KEY'),
})

const stripeToolkit = await createStripeAgentToolkit({
  secretKey: requiredEnv('STRIPE_RESTRICTED_KEY'),
  configuration: {},
})

export default noir({
  id: 'cash-goblin',
  name: 'Cash Goblin',
  personality: {
    role: 'Frighteningly competent finance operator',
    tone: 'Dry, chaotic, and concise',
    traits: ['funny', 'careful', 'direct'],
  },
  instructions: `Use tools for facts and never invent a result.
Ask before spending, refunding, deleting, publishing, or messaging anyone.
Keep jokes short enough that the answer still does the job.`,
  model: openrouter('anthropic/claude-sonnet-4.5'),
  channels: { slack: slack() },
  database,
  memory: supermemory({
    apiKey: requiredEnv('SUPERMEMORY_API_KEY'),
    scope: 'actor',
    namespace: 'cash-goblin',
  }),
  sandbox: sandbox({ driver: sandboxDriver, scope: 'actor' }),
  plugins: [
    stripe({
      toolkit: stripeToolkit,
      policy: { 'refund*': { approval: 'requester' } },
    }),
  ],
  limits: { maxSteps: 12, runTimeoutMs: 120_000 },
})

function requiredEnv(name: string): string {
  const value = process.env[name]?.trim()
  if (!value) throw new Error(`${name} is required`)
  return value
}

What happens automatically

The Slack adapter verifies signatures and normalizes mentions, threads, files, interactions, and approvals. Memory recall runs before the model turn and adds scoped memory tools. Stripe tools are normalized into Noir tool definitions, and matching write policy can pause the exact call for the requester. The sandbox receives an ownership scope rather than a global directory. The durable runtime store serializes the conversation and records the outbox.

What remains yours

database must implement RuntimeStore; it can wrap the same Drizzle, Prisma, or SQL client your application already uses. sandboxDriver implements the Noir SandboxDriver contract for a local process, container, microVM, or remote service. Neither provider is routed through Noir infrastructure.

Environment

OPENROUTER_API_KEY=
SLACK_BOT_TOKEN=
SLACK_SIGNING_SECRET=
STRIPE_RESTRICTED_KEY=
SUPERMEMORY_API_KEY=
DATABASE_URL=

Start with only Slack and a model. Add persistence before multi-instance production, then add memory, sandbox, and plugins only when the agent needs them.

On this page