> ## Documentation Index
> Fetch the complete documentation index at: https://docs.marginfront.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Spend Controls

> Read and manage your team's AI coding spend caps from the Node SDK.

# Spend Controls

The SDK gives you six methods for working with **spend caps**: the limits your company sets on its own AI coding spend. These are the same caps the dashboard's Money tab shows, readable and writable from code.

This page covers:

1. Listing caps
2. Reading spend against the caps
3. Reading coverage ("how many developers are protected?")
4. Creating a cap
5. Updating a cap
6. Deleting a cap

If you're new to spend caps, start with the [Spend Controls API reference](/api-reference/spend-controls) for a plain-English explanation of scopes, modes, and how enforcement works.

***

## Setup

```typescript theme={null}
import { MarginFrontClient } from "@marginfront/sdk";

const mf = new MarginFrontClient(process.env.MF_API_SECRET_KEY);
```

The three read methods work with any secret key for your organization. The three write methods (`create`, `update`, `delete`) need a key that belongs to an **owner or finance** user. A key without that role gets the server's own plain-English `403`, surfaced as-is.

***

## `spendControls.list`

Lists every cap policy for your organization. Each cap carries a `sentence` field: the cap written out as one plain sentence, built by the server.

### Signature

```typescript theme={null}
await mf.spendControls.list(): Promise<SpendCap[]>
```

### Example

```typescript theme={null}
const caps = await mf.spendControls.list();
caps.forEach((c) => console.log(c.sentence));
// → "Stop AI spend at $5,000 across the whole team per month"
```

***

## `spendControls.spend`

Returns your team's internal coding-agent spend for the current `day`, `week`, or `month` window (computed in UTC on the server). Pass a developer's `customerExternalId` (their email) to also get that developer's number.

### Signature

```typescript theme={null}
await mf.spendControls.spend(
  period: "day" | "week" | "month",
  customerExternalId?: string,
): Promise<SpendReadback>
```

### Example

```typescript theme={null}
const week = await mf.spendControls.spend("week");
console.log(week.org.spentUsd); // e.g. 42.5, or null if nothing priced yet

const alice = await mf.spendControls.spend("week", "alice@acme.com");
console.log(alice.dev?.spentUsd);
```

`spentUsd` is `null` when there's no priced usage in the window. A `null` means "nothing to total yet," never a fake `0`. Check `unpricedEventCount`: if it's above zero, some events couldn't be priced and the true spend is higher than `spentUsd` shows.

***

## `spendControls.coverage`

Answers "N of M developers armed" over the trailing 7 days: how many developers' machines have cap enforcement turned on, out of every developer seen at all.

### Signature

```typescript theme={null}
await mf.spendControls.coverage(): Promise<SpendCoverage>
```

### Example

```typescript theme={null}
const { armed, total } = await mf.spendControls.coverage();
console.log(`${armed} of ${total} developers armed`);
```

`asOf` is `null` when there's been no recent activity. The server never makes up a timestamp.

***

## `spendControls.create`

Creates a cap. Owner or finance key required.

### Signature

```typescript theme={null}
await mf.spendControls.create(params: CreateSpendCapParams): Promise<SpendCap>
```

### Example

```typescript theme={null}
// A whole-team ceiling
const cap = await mf.spendControls.create({
  scope: "org",
  amountUsd: 5000,
  period: "month",
  mode: "track",
});
console.log(cap.sentence);
// → "Stop AI spend at $5,000 across the whole team per month"

// A per-developer cap. scopeValue is the developer's customerExternalId (email).
await mf.spendControls.create({
  scope: "dev",
  scopeValue: "alice@acme.com",
  amountUsd: 200,
  period: "week",
  mode: "enforce",
});
```

Every cap covers all AI tools; the SDK sends `providerScope: "all"` for you. A developer cap can never be set higher than the whole-team ceiling: the server rejects it with a plain message you can show as-is.

***

## `spendControls.update`

Adjusts an existing cap's `amountUsd`, `period`, `mode`, `coolOffHours`, or `alertThresholds`. Owner or finance key required. A cap's identity (its scope and target) can't be changed; delete and recreate instead.

### Signature

```typescript theme={null}
await mf.spendControls.update(id: string, patch: UpdateSpendCapParams): Promise<SpendCap>
```

### Example

```typescript theme={null}
const cap = await mf.spendControls.update(capId, { amountUsd: 8000 });
console.log(cap.sentence); // the refreshed sentence
```

***

## `spendControls.delete`

Deletes a cap. Owner or finance key required. The response echoes the removed cap's sentence so you can confirm what was deleted.

### Signature

```typescript theme={null}
await mf.spendControls.delete(id: string): Promise<DeleteSpendCapResponse>
```

### Example

```typescript theme={null}
const { sentence } = await mf.spendControls.delete(capId);
console.log(`Removed: ${sentence}`);
```

***

## The types

```typescript theme={null}
type SpendCapPeriod = "day" | "week" | "month";
type SpendCapMode = "track" | "enforce";

interface SpendCap {
  id: string;
  scope: "org" | "team" | "dev";
  scopeValue: string | null;
  providerScope: string;
  amountUsd: number;
  period: SpendCapPeriod;
  mode: SpendCapMode;
  coolOffHours: number | null;
  alertThresholds: number[];
  createdByUserId: string | null;
  createdAt: string;
  updatedAt: string;
  sentence: string;
}

interface CreateSpendCapParams {
  scope: "org" | "dev";
  scopeValue?: string;
  providerScope?: "all";
  amountUsd: number;
  period: SpendCapPeriod;
  mode: SpendCapMode;
  coolOffHours?: number;
  alertThresholds?: number[];
}

interface UpdateSpendCapParams {
  amountUsd?: number;
  period?: SpendCapPeriod;
  mode?: SpendCapMode;
  coolOffHours?: number;
  alertThresholds?: number[];
}

interface SpendScopeTotal {
  customerExternalId?: string;
  spentUsd: number | null;
  eventCount: number;
  unpricedEventCount: number;
}

interface SpendReadback {
  period: SpendCapPeriod;
  window: { start: string; end: string };
  asOf: string;
  org: SpendScopeTotal;
  dev: SpendScopeTotal | null;
}

interface SpendCoverage {
  armed: number;
  total: number;
  asOf: string | null;
}

interface DeleteSpendCapResponse {
  deleted: boolean;
  sentence: string;
}
```

Note the read types accept a wider `scope` vocabulary than `create` does. That's deliberate: `create` takes `org` or `dev` only, while `list` can return older rows without breaking your types if the vocabulary ever widens.
