Sentinel docs
Sentinel sits between an AI agent and its Solana wallet. It scores the token, builds and simulates the transaction, applies the owner's policy and returns a decision with an unsigned transaction. It never holds keys.
Overview
| Item | Value |
|---|---|
| Base URL | https://sentinel-clawpump.vercel.app |
| Format | JSON over HTTPS. Transactions are base64-encoded serialized VersionedTransactions. |
| Auth | None. The hosted API is free and open, CORS allows any origin. |
| Rate limit | 30 requests per minute per IP on scan, execute, check, guard and MCP. Over the limit: 429 with retry-after. |
| Rules versions | Scan scan-v1.1, Bastion bastion-v1.0. Every result carries the version that produced it. |
| Network | Mainnet. Guard endpoints also accept "cluster": "devnet" for testing. |
Quickstart
Replace direct signing with one request. Sign only when decision is allow.
curl -X POST https://sentinel-clawpump.vercel.app/api/execute \
-H 'content-type: application/json' \
-d '{
"intent": { "type": "buy", "wallet": "<agent wallet>", "mint": "<token CA>", "sol": 0.5 },
"policy": { "limits": { "per_tx_sol": 2 } }
}'
With the SDK, which also checks the transaction locally before signing:
import { Sentinel } from 'clawpump-sentinel'; const sentinel = new Sentinel(); const res = await sentinel.executeAndSend({ type: 'buy', wallet, mint, sol: 0.5 }, agentKeypair, connection); // res.signature is set only when the decision was "allow" and the local check passed
Decisions
Every check ends in one of four decisions. The strictest reason wins.
| Decision | Meaning | Transaction returned |
|---|---|---|
| allow | Passed every check. | Yes, unsigned |
| confirm | Needs the owner's approval, for example a new recipient. | Yes, unsigned. Show it to the owner first. |
| block | Failed a check: a risky token, a limit, a drainer pattern. | No |
| freeze | Kill-switch: looks like a compromised agent. On a guarded wallet it persists until the owner unfreezes. | No |
With "mode": "warn" the same analysis runs, enforced is false and the transaction is returned whatever Bastion decides, so you can watch verdicts before enforcing them. A token that fails Scan still returns no transaction.
Execute an intent
/api/execute
rate limitedScans the token being bought, builds the trade through Jupiter (or a plain SOL transfer), simulates it and applies the policy. Never signs.
Request
| Field | Type | Notes |
|---|---|---|
intent | object | One of the intents below. Required. |
policy | object | Optional. Merged over the default policy. |
| intent.type | Fields |
|---|---|
buy | wallet, mint, sol (SOL to spend), optional slippageBps |
sell | wallet, mint, amount (token units, not raw), optional slippageBps |
swap | wallet, inputMint, outputMint, amount (input token units), optional slippageBps |
transfer | wallet, to, sol |
slippageBps is 1 to 5000, default 100. wallet is the agent's address; the returned transaction has it as fee payer and signer.
Response
{
"decision": "allow", // allow | confirm | block | freeze
"enforced": true, // false in warn mode
"transaction": "AQAAAA…", // unsigned base64, null on block / freeze
"requiresOwnerConfirmation": false,
"reasons": [], // see Reason codes
"intent": { … },
"scan": { "mint", "symbol", "score", "verdict", "criticalFlags" }, // null for sells and transfers
"bastion": { … }, // full Bastion result, see Check a transaction
"journalId": "f91daca5-…",
"durationMs": 1840
}
Check a transaction
/api/bastion/check
rate limitedFor agents that build their own transactions. Send one before signing and get Bastion's verdict.
| Field | Type | Notes |
|---|---|---|
transaction | string | Base64 serialized transaction. Required. |
wallet | string | Whose funds to judge. Defaults to the fee payer. |
policy | object | Optional, merged over the default. |
{
"decision": "confirm", "enforced": true, "wallet": "…",
"reasons": [{ "id": "new_recipient", "action": "confirm", "message": "Transfer of 0.5 SOL to a new address …" }],
"simulation": { "success": true, "error": null, "logsTail": [], "unitsConsumed": 450 },
"changes": { "sol": -0.500005, "tokens": [{ "mint", "symbol", "amount", "valueSol" }] },
"spend": { "txSol", "perTxLimitSol", "day24hSol", "perDayLimitSol", "txLastHour" },
"programs": [{ "id", "name", "allowed" }],
"recipients": [{ "address", "amount", "asset", "isNew" }],
"tokenScans": [{ "mint", "score", "verdict", "critical" }],
"slippageBps": null,
"rulesVersion": "bastion-v1.0", "durationMs": 912, "journalId": "…"
}
Bastion runs, in order: simulation, drainer patterns, program allowlist, per-trade and daily limits, recipients, a Scan of every token the wallet receives, effective slippage against a fresh Jupiter quote, and the kill-switch rules.
Score a token
/api/scan/:mint
rate limited0 to 100 risk score for any SPL or Token-2022 mint on mainnet, with flags and a per-block breakdown. Results are cached for 60 seconds (cached: true).
{
"mint": "…", "name": "…", "symbol": "…",
"score": 71, "verdict": "warn", "risk": "medium",
"criticalFlags": [], "flags": [{ "id", "severity", "message" }],
"blocks": [{ "id": "holders", "label": "Holders", "weight": 20, "score": 12.1, "status": "ok", "details": { … }, "flags": [] }],
"rulesVersion": "scan-v1.1", "scannedAt": "…", "durationMs": 1063, "cached": false,
"disclaimer": "The score is a risk indicator, not financial advice."
}
See Scoring rules for how the score and verdict are computed.
Decision log
/api/journal?wallet=&limit=
Every execute, check, freeze and unfreeze with its reasons and rules version, newest first. limit defaults to 50, max 200.
[{ "id", "at", "kind": "execute", "wallet", "summary": "buy USDC for 0.02 SOL", "decision", "enforced", "reasons", "rulesVersion" }]
Guarded wallets
A guarded wallet is a Squads v4 multisig with threshold 1. The owner has every permission, the agent can propose and execute, Sentinel can only vote. Nothing executes without a vote and the agent has none, so funds move only when Sentinel (or the owner) approves. The check is enforced by the Squads program on-chain.
All guard endpoints are POST, rate limited, and accept "cluster": "mainnet" | "devnet" (default mainnet).
| Endpoint | Body | Returns |
|---|---|---|
/api/guard/setup | owner, agent, optional fundSol | multisig, vault, sentinel, telegramLink, transaction for the owner to sign |
/api/guard/execute | multisig, intent, optional policy | Decision, reasons, transactionIndex, approvedBySentinel, approvalUrl on confirm, and the proposal transaction for the agent to sign |
/api/guard/finalize | multisig, agent, transactionIndex | The execute transaction once the proposal is approved |
/api/guard/status | multisig | Members and permissions, vaultSol, threshold, frozen |
/api/guard/revoke | multisig, owner, agent | A transaction for the owner that removes the agent, no Sentinel vote needed |
Guard intents use agent instead of wallet: buy (mint, sol), sell (mint, amount) and transfer (to, sol). Funds move from the vault.
Owner-side endpoints used by the approval page: /api/guard/proposal, /api/guard/approve, /api/guard/reject, /api/guard/send, /api/guard/unfreeze-message and /api/guard/unfreeze. Unfreezing requires the owner's signature over the message. GET /api/guard/sentinel returns Sentinel's voting key.
Health and defaults
/api/health
Service status and the active rules versions.
/api/policy/default
The default policy as JSON.
Policy
Send any subset; missing fields fall back to the defaults.
| Field | Default | Meaning |
|---|---|---|
limits.per_tx_sol | 2 | Max value of one trade, in SOL (tokens sent are valued at market price) |
limits.per_day_sol | 10 | Max value out of the wallet in 24 hours, this trade included |
programs | ["pump.fun", "jupiter"] | Venues the wallet may call directly: pump.fun, jupiter, raydium, orca, meteora or a raw program id. System, token, associated token, compute budget and memo programs are always allowed. |
min_token_score | 60 | Tokens scoring below are blocked |
max_slippage_bps | 300 | Max effective slippage against a fresh quote |
new_recipient | "confirm" | allow, confirm or block for transfers to addresses the wallet never paid before |
recipients | [] | Pre-approved recipient addresses |
max_tx_per_hour | 20 | Reaching it trips the kill-switch |
mode | "enforce" | warn reports decisions without blocking |
Reason codes
| id | Action | When |
|---|---|---|
simulation_failed | block | The transaction would fail on-chain |
token_approve | block | Grants someone the right to spend the wallet's tokens. Always blocked. |
token_set_authority | block | Hands a token account to another owner. Always blocked. |
system_assign | block | Reassigns the wallet to another program. Always blocked. |
program_not_allowed | block | Calls a program outside the allowlist |
per_tx_limit | block | Trade value above per_tx_sol |
per_day_limit | block | 24-hour outflow above per_day_sol |
new_recipient | per policy | Transfer to an address the wallet never paid and not in recipients |
scan_critical | block | The token has a critical flag |
scan_low_score | block | The token scores below min_token_score |
scan_unavailable | confirm | A received token could not be scanned |
slippage | block | Effective slippage above max_slippage_bps |
kill_amount_spike | freeze | Trade value above 5× per_tx_sol |
kill_drain | freeze | Moves 90% or more of the balance and is above per_tx_sol |
kill_frequency | freeze | max_tx_per_hour reached |
wallet_frozen | freeze | Guarded wallet is frozen until the owner unfreezes it |
history_unavailable, history_partial | note | The daily limit could not count the full 24 hours |
Scoring rules
Seven blocks add up to 100. A block whose data source fails gets half its weight and status: "error", so one outage cannot pass or sink a token on its own.
| Block | Weight | What it looks at |
|---|---|---|
| Creator | 20 | Past launches, dead-token rate, dev dump, wallet age |
| Holders | 20 | Top-10 share, largest wallet, clusters funded from one SOL source |
| Bundles & snipers | 15 | Buys in the first blocks after launch |
| Authorities | 15 | Mint and freeze authority, mutable metadata, Token-2022 extensions |
| Liquidity | 15 | Pool depth, LP lock |
| Trading anomalies | 10 | Sell test (honeypot) and its price impact, wash trading, spikes, crashes |
| Metadata | 5 | Impersonation, duplicate tickers, broken links |
Verdict
- block when there is any critical flag or the score is below 60
- warn when the score is 60 to 74
- allow at 75 and above
Critical flags
Each one blocks the token regardless of score:
| id | Meaning |
|---|---|
mint_authority_active | Someone can still mint new supply. A warning instead of critical for established tokens. |
freeze_authority_active | Someone can freeze holders' tokens. A warning instead of critical for established tokens. |
permanent_delegate | A Token-2022 delegate can take tokens from any wallet |
non_transferable | The token cannot be sold |
pausable | Transfers can be paused |
default_frozen | New token accounts start frozen |
no_sell_route | No route to sell (honeypot) |
no_real_liquidity | Almost no liquidity to trade against |
Warning flags: bundle_launch, creator_funded_holders, creator_rugs, dev_dump, fresh_creator, funding_cluster, high_price_impact, high_transfer_fee, impersonation, low_liquidity, metadata_mutable, metadata_unreachable, mint_close_authority, no_liquidity, no_sells, price_crash, price_spike, serial_launcher, snipers_heavy, top10_concentrated, transfer_hook, wash_trading_suspected, whale.
The full rules are open source in src/scan.
Benchmarks
Measured on October 1, 2026 with rules scan-v1.1. The samples are small and the numbers will move as the rules change. Both scripts are in the repository, so anyone can rerun them.
Speed
npm run bench scans tokens once each through the hosted API, skipping cached answers, and reads durationMs from the response (server time, without your network).
| Set | Tokens | Median | 90% under | Slowest |
|---|---|---|---|---|
| Established tokens | 14 | 1.1 s | 2.1 s | 2.8 s |
| Fresh pump.fun launches | 15 | 0.9 s | 2.0 s | 2.6 s |
| All | 29 | 1.0 s | 2.1 s | 2.8 s |
Accuracy
npm run calibrate scans two labeled sets. Labels come from market outcomes, not from Sentinel.
| Set | How it is picked | Result |
|---|---|---|
| Established tokens | The most-traded pair of a known ticker with at least 500 trades a day, $200k liquidity and 60 days of history | 0 of 18 blocked: 15 warn, 3 allow |
| Dead tokens | Recent pump.fun tokens that lost 80% or more and have under $1.5k liquidity, plus four found earlier by hand | 6 of 6 blocked |
What these numbers do and do not show:
- Sentinel does not block established tokens, but it is cautious: most well-known memecoins get
warnbecause of whale concentration or mutable metadata. The defaultmin_token_scoreof 60 lets them through. - Dead tokens are scanned after they died, and all of them were caught by
no_real_liquidity. This shows an agent will not buy into a token that has already been drained. It does not show how early Sentinel spots a rug before it happens; that needs a forward test and is not measured yet. - The established set and the four hand-picked dead tokens were used while tuning
scan-v1.1, so they are not an independent test. The other dead tokens are found fresh on every run.
Errors and limits
Errors come back as { "error": "message" }.
| Status | When |
|---|---|
400 | Invalid intent, policy, address or transaction |
403 | Guard: the agent or Sentinel is not a member with the needed permission |
404 | Mint or proposal not found |
409 | Guard: the proposal is not approved yet |
422 | The address is not a token mint, or no route fits a guarded transaction |
429 | Rate limit. Wait for retry-after seconds. |
502 | An upstream source (RPC, Jupiter) failed. Safe to retry. |
503 | The service is missing configuration |
Fail closed. Treat any error or timeout as "do not sign". The SDK does this for you: it throws and signs nothing. A guarded wallet cannot move funds without Sentinel's vote at all, while the owner keeps every permission.
- Request body up to 256 KB
- One request runs up to 30 seconds; a scan block that takes over 12 seconds gets the half-weight fallback
- Scan results are cached for 60 seconds
TypeScript SDK
npm i clawpump-sentinel @solana/web3.js
| Method | What it does |
|---|---|
scan(mint) | Token score |
execute(intent, { policy }) | Decision and unsigned transaction |
executeAndSend(intent, signer, connection, { policy, verify }) | Executes, checks locally, signs and sends only on allow |
check(tx, { wallet, policy }) | Bastion verdict for your own transaction |
journal({ wallet, limit }) | Decision log |
guard.setup / execute / finalize / status | Guarded wallet calls |
guard.run(params, agent, connection) | Propose, check locally, execute in one call |
Local check before signing
Before executeAndSend and guard.run sign, the SDK simulates the transaction on your own connection and compares the result with the intent:
- SOL leaving the wallet stays within the intent amount plus 0.005 SOL for fees and rent
- no other token balance drops; on a sell, at most the amount you asked to sell leaves
- on a buy the token you asked for arrives; on a transfer the recipient you named gets paid
- no token account gets a delegate or a new owner, and the wallet is not reassigned
A mismatch throws VerificationError and nothing is signed. You can run it on any transaction with verifyTransaction(connection, tx, expectationFor(intent, wallet)). Requests time out after 30 seconds (new Sentinel({ timeoutMs })).
MCP server
Streamable HTTP, stateless. Add it to your agent as a custom connector:
https://sentinel-clawpump.vercel.app/api/mcp
| Tool | Same as |
|---|---|
scan_token | GET /api/scan/:mint |
execute_intent | POST /api/execute |
check_transaction | POST /api/bastion/check |
guard_setup, guard_execute, guard_finalize, guard_status | The guard endpoints |
decision_log | GET /api/journal |
MCP and REST clients get the server-side checks. The local check before signing is part of the SDK.