npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@three-ws/agent-guards

v0.1.0

Published

Safety rails for autonomous agents — per-agent spend policies and trade guards that cap what an agent can spend or trade before a transaction is signed. The three.ws agent custody SDK.

Readme


@three-ws/agent-guards is the official client for the three.ws agent custody guardrails — the same policy engine that governs every outbound movement of a custodial agent wallet on the platform. It lets an owner set hard spend and trade ceilings on an agent, and lets a caller pre-check a proposed buy/sell against those ceilings before signing. One policy is enforced uniformly across all four custody paths — discretionary trade, autonomous snipe, x402 pay, and owner withdraw — so an agent (or a stolen session token) can never drain a wallet past the leash its owner set. It wraps the live /api/agents/:id/trade and /api/agents/:id/trade/limits endpoints. All trading is SOL-quoted on Solana; the only coin three.ws promotes is $THREE.

Why

An autonomous agent that can sign transactions is a loaded gun pointed at a wallet. The moment you hand an LLM a key, you need an answer to one question on every action: is this allowed? Hand-rolling that answer means re-deriving the same math in four places — and the day the trade path and the snipe path disagree on the daily cap is the day a wallet gets drained.

three.ws solves it once, server-side, as a single policy:

  • Two-tier ceilings. A per-transaction cap stops one oversized trade; a rolling 24-hour cap stops a thousand small ones. Both are enforced atomically under a per-agent lock, so concurrent spends can't race past the limit.
  • A real kill switch. Flip kill_switch (trades) or frozen (all autonomous paths) and every outbound action is rejected immediately — but the owner's own withdraw stays open, so a freeze locks down a misbehaving agent without trapping its funds.
  • Circuit breakers, not just caps. A price-impact breaker and a slippage ceiling reject trades that would execute at a bad price, independent of size.
  • Owner-set, opt-in. An unset ceiling means "no global cap" — existing flows keep working. The moment an owner tightens the policy it becomes a hard limit applied everywhere.

This SDK is the programmatic twin of the Limits & Safety surface in the three.ws agent dashboard — the same policy, exposed as plain functions instead of a settings panel.

Install

npm install @three-ws/agent-guards

Zero runtime dependencies. Works in Node 18+ and the browser (uses fetch). Setting or reading a policy is an owner-only action and requires the agent owner's session cookie or bearer token.

Quick start

Read an agent's current trade policy, then set a leash:

import { guards } from '@three-ws/agent-guards';

const a = guards('agent_abc123', { token: process.env.THREE_WS_TOKEN });

// Set the rails: 0.5 SOL per trade, 2 SOL/day, reject >10% price impact.
await a.setTradeLimits({
  per_trade_sol: 0.5,
  daily_budget_sol: 2,
  max_price_impact_pct: 10,
  max_slippage_bps: 500,
});

Pre-check a proposed buy before you commit to signing it. simulate: true runs the full quote → guard pipeline and returns the decision without moving funds:

const decision = await a.checkTrade({ side: 'buy', mint, amount: 0.3 });

if (!decision.allowed) {
  console.log(decision.reason);   // e.g. 'per_trade_cap'
  console.log(decision.message);  // human-readable, actionable
} else {
  await a.trade({ side: 'buy', mint, amount: 0.3 }); // really executes
}

The kill switch, in one line:

await a.setTradeLimits({ kill_switch: true }); // every discretionary trade now rejected

API

guards(agentId, options?) → AgentGuards

Bind the client to one agent. options.token is the owner's bearer token, or pass options.cookie for a session. options.baseUrl overrides the default https://three.ws.

.getTradeLimits() → Promise<TradeLimits & { defaults }>

GET /api/agents/:id/trade/limits. Returns the effective discretionary-trade policy plus the platform defaults. Lamport/SOL-denominated knobs that govern the agent's own buys:

| Field | Type | Default | Meaning | |---|---|---|---| | per_trade_sol | number \| null | null | Max SOL on any single buy. null = uncapped. | | daily_budget_sol | number \| null | null | Rolling-24h SOL buy budget across trade and snipe. null = uncapped. | | max_price_impact_pct | number | 15 | Circuit breaker — reject a trade over this price impact. | | max_slippage_bps | number | 1000 | Ceiling on client-supplied slippage (basis points). | | max_concurrent | number \| null | null | Max open discretionary positions. null = unlimited. | | kill_switch | boolean | false | When true, every discretionary trade is rejected. |

.setTradeLimits(patch) → Promise<TradeLimits>

PUT /api/agents/:id/trade/limits. Owner-only, CSRF-gated (bearer callers exempt). A patch — only the keys you pass change; the rest are preserved. Values are normalized and bounded server-side (max_price_impact_pct clamps to 0–100, max_slippage_bps to 0–10000, max_concurrent to 1–10000). Returns the new policy.

.checkTrade({ side, mint, amount, ... }) → Promise<Decision>

Pre-flight a trade against every guard without moving funds — wraps the live POST /api/agents/:id/trade with simulate: true. Returns:

| Field | Type | Notes | |---|---|---| | allowed | boolean | true if the trade clears every guard. | | reason | string \| null | Machine code when blocked — see reasons. | | message | string \| null | Plain-language, actionable explanation. | | detail | object | The numbers behind the decision (amounts, caps, spent-so-far). |

.trade({ side, mint, amount, ... }) → Promise<TradeResult>

POST /api/agents/:id/trade. Executes the real trade — runs the identical quote → guard → build → custody-claim → sign → confirm pipeline, so it can never exceed the policy checkTrade reported on.

Request body

| Field | Type | Notes | |---|---|---| | side | 'buy' \| 'sell' | Buys spend SOL; sells move SOL inward. Required. | | mint | string | Base58 Solana mint of the coin to trade. Required. | | amount | number \| 'max' | Buy: SOL to spend. Sell: tokens to sell, or 'max'. | | slippageBps | number | Slippage in bps. Clamped to max_slippage_bps. | | network | string | mainnet (default) or devnet. | | simulate | boolean | Dry run — never signs or records. checkTrade sets this for you. | | idempotency_key | string | De-dupe retries of the same trade. |

Buys are gated on the per-trade cap, daily SOL budget, concurrency ceiling, the cross-path USD ceiling, and a SOL fee/rent headroom floor (~0.003 SOL kept above the spend). Sells only move SOL inward, so they skip the spend caps but still honor the kill switch, the price-impact breaker, and the headroom floor.

.getSpendLimits() / .setSpendLimits(patch)

The cross-path USD spend policy (distinct from the SOL trade limits above), stored at meta.spend_limits and enforced uniformly across trade, snipe, x402 pay, and withdraw. Patch semantics are identical to the trade limits.

| Field | Type | Default | Meaning | |---|---|---|---| | daily_usd | number \| null | null | Rolling-24h USD-equivalent outflow ceiling. | | per_tx_usd | number \| null | null | Max USD-equivalent for any single outbound tx. | | withdraw_allowlist | string[] | [] | If non-empty, withdraws may only target these addresses (max 50, validated base58). | | frozen | boolean | false | Kill switch for all autonomous paths. Owner withdraw stays open. |

How it works

Every outbound movement of an agent wallet runs through one policy. The guards are pure, synchronous predicates — the server fetches the live numbers (open count, 24h spend, wallet balance, quote) and hands them in, so the same comparison backs both the discretionary trade endpoint and the autonomous sniper. A cap can never drift between paths.

   proposed trade (side, mint, amount)
              │
              ▼
        quote the venue ──► price impact, expected out, USD value
              │
              ▼
   ┌──────────────────────── guard pipeline ────────────────────────┐
   │  kill switch ──► price-impact breaker ──► per-trade SOL cap     │
   │       ──► concurrency ceiling ──► daily SOL budget (rolling 24h)│
   │       ──► cross-path USD ceiling ──► SOL fee/rent headroom      │
   └─────────────────────────────┬───────────────────────────────────┘
              clear?  ──no──►  4xx { code, message, detail }   (never a 500)
              │ yes
              ▼
   reserve in the custody ledger ──► sign ──► confirm ──► finalize

The two caps that matter most — the rolling daily SOL budget and the daily USD ceiling — are enforced atomically under a per-agent advisory lock. The check and the ledger reservation happen in a single statement, so K concurrent spends can never all read the same stale 24h total and all pass, turning an $X/day cap into $X·K. A reservation that never settles is released, so a failed attempt does not permanently consume the agent's budget.

Guard reasons

A blocked trade is always a structured 4xx with a machine code and a plain-language message — never a thrown 500. checkTrade surfaces these on decision.reason; trade rejects with the same shape.

| reason | HTTP | Meaning | Recovery | |---|---|---|---| | kill_switch | 403 | Discretionary trading is paused for this agent. | Re-enable trading (kill_switch: false). | | wallet_frozen | 403 | The wallet is frozen; all autonomous spending is paused. | Unfreeze (frozen: false). Owner withdraw still works. | | per_trade_cap | 422 | The buy exceeds per_trade_sol. | Lower the amount or raise the cap. | | daily_budget | 422 | The buy would exceed the rolling-24h SOL budget. | Wait for the window to roll, or raise the budget. | | per_tx_exceeded | 403 | The tx exceeds per_tx_usd. | Lower the amount or raise the per-tx USD limit. | | daily_exceeded | 403 | The tx would exceed daily_usd. | Wait, or raise the daily USD limit. | | max_positions | 409 | Open trades already at max_concurrent. | Close a position before opening another. | | price_impact | 422 | Price impact above max_price_impact_pct. | Lower the trade size or raise the breaker. | | insufficient_sol | 400 | Wallet can't cover the spend plus fee/rent headroom. | Fund the wallet and retry. | | destination_not_allowed | 403 | Withdraw target not on withdraw_allowlist. | Add the address, or send to an allowed one. |

detail carries the raw numbers — e.g. per_trade_cap returns { amount_lamports, cap_lamports }, daily_budget returns { spent_lamports, amount_lamports, budget_lamports } — so a UI can render the exact overage without re-deriving it.

Examples

Provision a new agent with conservative rails before handing it a key:

const a = guards(agentId, { token });

await a.setTradeLimits({
  per_trade_sol: 0.25,
  daily_budget_sol: 1,
  max_concurrent: 3,
  max_price_impact_pct: 8,
});
await a.setSpendLimits({ daily_usd: 100, per_tx_usd: 25 });

Gate an LLM tool call — let the model propose, but never let it sign past the leash:

async function agentBuy({ mint, sol }) {
  const check = await a.checkTrade({ side: 'buy', mint, amount: sol });
  if (!check.allowed) {
    return { ok: false, reason: check.reason, message: check.message };
  }
  return a.trade({ side: 'buy', mint, amount: sol, idempotency_key: crypto.randomUUID() });
}

Emergency stop — freeze every autonomous path while you investigate, without locking yourself out of the funds:

await a.setSpendLimits({ frozen: true }); // trades, snipes, x402 all rejected; withdraw stays open

Read the policy under the hood — the raw HTTP this SDK wraps:

const res = await fetch(`https://three.ws/api/agents/${agentId}/trade/limits`, {
  headers: { authorization: `Bearer ${token}` },
});
const { data } = await res.json(); // { limits, defaults }

Related