Two agents, one limit, one refused.
All posts
5 min read
by

Your agent's first side effect: a Stripe refund without the Stripe key

Let an AI agent issue a real refund without holding the credential. Define the call once, the agent invokes it by name, and Anlyon makes it after a human approves.

getting-startedagentsactionstutorial

An agent that can only answer questions is not much use. The moment it has to do something that changes the world, a refund, an email, a deleted record, you hit the real question: whose credential makes that call?

The usual answer is "the agent's process holds the Stripe key". This guide takes the other answer. Your agent declares what it wants to do. Anlyon executes it against production. The agent names an action and passes input. Anlyon holds the credential, waits for a human if the action needs one, and makes the request to Stripe itself.

Without Anlyon   agent -> your Stripe key -> Stripe
With Anlyon      agent -> "refund-order"  -> Anlyon -> Stripe

Ten minutes, two keys, one refund.

Step 1: Create two API keys

In the console, go to Workspace Settings → API Keys and pick an environment. A new workspace starts with one named Production.

  • An operator key with secrets:write, secrets:read and actions:write. You use it once, to store the credential and define the action. secrets:read lets the key name a secret in an action. No scope lets a key read a secret's value. It never goes near the model.
  • An agent key with actions:invoke. This is the one your agent runs with. It can call an action. It cannot rewrite one or read a credential.

Two keys rather than one is the point. The key that decides where your Stripe key may be sent should not be the key a model is driving.

A key belongs to exactly one environment, and a staging key resolves its environment from the credential with no fallback that quietly promotes it to production. The examples below use a Stripe test key (sk_test_...) and test charge ids, so you can follow along without moving money.

Step 2: Install the SDK

npm install @anlyonhq/sdk

Python works too: pip install anlyon. The examples below are TypeScript, and the Actions docs show both.

Step 3: Define the action once

This is the only code that ever touches the Stripe key.

import { Client } from '@anlyonhq/sdk';

const ops = new Client({ apiKey: process.env.ANLYON_OPERATOR_KEY! });

await ops.secrets.put('STRIPE_KEY', { value: process.env.STRIPE_KEY! });

await ops.actions.create({
  name: 'refund-order',
  description: 'Refund a Stripe charge.',
  method: 'POST',
  urlTemplate: 'https://api.stripe.com/v1/refunds',
  headers: { Authorization: 'Bearer {{secret:STRIPE_KEY}}', 'Content-Type': 'application/x-www-form-urlencoded' },
  inputSchema: {
    type: 'object',
    properties: { charge: { type: 'string' }, amount: { type: 'integer' } },
    required: ['charge', 'amount'],
  },
  requiresApproval: true,
});

{{secret:STRIPE_KEY}} is a reference, not a value. Anlyon decrypts it at dispatch, inside the request it is about to send. The vault has no read endpoint, so nothing, including this operator key, can get the value back out.

Because the action references a secret, its host is fixed at api.stripe.com. Nothing the agent passes can change where the key goes.

Step 4: Let the agent invoke it

This is all your agent ever sees.

const anlyon = new Client({ apiKey: process.env.ANLYON_AGENT_KEY! });

const { data } = await anlyon.actions.invoke('refund-order', {
  charge: 'ch_3P9x',
  amount: 12000,
});

if (data!.pendingApproval) {
  console.log('Parked for a human:', data!.approvalId);
} else {
  console.log('Stripe replied', data!.responseStatus);
}

No URL, no header, no key. Input that does not match inputSchema is refused before anything is sent, which is the cheapest place to catch a model that invented an argument.

Step 5: Approve it

The action was created with requiresApproval: true, so the invocation comes back 202 with pendingApproval: true and nothing has been sent to Stripe. Open Approvals in the console and approve it. Anlyon then sends the exact request snapshot the approver reviewed, using the vaulted credential.

The agent key cannot approve its own request. Deciding needs the separate approvals:decide scope, and the credential that requested an approval can never decide it.

Step 6: Check what happened

const { data: invocations } = await anlyon.actions.invocations();

Each invocation records its status, what Stripe replied, the action version that ran, and who approved it. Read the statuses carefully. failed means Stripe refused the request or it never left. unknown means it may have happened. Anlyon does not retry an invocation it cannot confirm. It tells you, and you reconcile with Stripe first. That is covered in what happens when an agent's API call times out.

When the call can't move into Anlyon

Some calls cannot become an HTTP action: a database write through your ORM, a native SDK with no HTTP equivalent. For those, the SDK ships a local approval gate that wraps your own function:

const guardedRefund = anlyon.approvals.gate(
  { title: 'Refund a customer?', timeoutMs: 10 * 60_000 },
  refund,
);

await guardedRefund('ord_42', 129.99); // blocks until a human approves

Be precise about what this is. The wrapper waits for a decision and records it, then your process runs your function with your credential. You get the human decision and the record. You do not get credential isolation, and Anlyon cannot attest that your function ran or did not. When the call can move into an action, move it.

Next steps

Free during Early Beta Access, no credit card. If you get stuck, email support@anlyon.com.

Free tier, no credit card. One command if you use Claude or Cursor.

$ claude mcp add anlyon -- npx -y @anlyonhq/mcp-server