sentinelshield-stellar
v0.1.0
Published
On-chain payment firewall for AI agents on Stellar — deploy, protect, and audit every payment with one npm install.
Maintainers
Readme
sentinelshield-stellar
On-chain payment firewall for AI agents on Stellar. Deploy your own instance, fund it, set your rules, and wire it into your agent. Three lines of code protect the entire wallet — even if the agent's instructions get compromised, its reasoning gets prompt-injected, or its keys get stolen.
Why this exists
AI agents with wallets are a footgun. One compromised prompt, one bad API response, one hallucinated tool call, and your agent drains its funds to an attacker. Passwords don't help — the agent is the one holding the keys.
SentinelShield is the HTTPS of AI agent money. You deploy a tiny Soroban contract that holds the funds. Your agent can only spend via that contract. The contract enforces your rules on every payment — allowlists, per-transaction caps, daily caps, pause switches, emergency recovery — on-chain, where nothing the agent does can bypass them.
Who is this for?
Any developer building an AI agent on Stellar that controls real funds:
- x402 servers that pay for per-request content
- MPP agents that open streaming payment channels
- Autonomous trading bots, API-paying assistants, customer-service agents with refund authority
- Anything where "what if this agent gets tricked?" is a question you worry about
Install once, deploy your contract, wire it in. Every project in the Stellar AI ecosystem needs this.
Install
npm install sentinelshield-stellarYou also need the Stellar CLI installed to deploy new contracts (brew install stellar-cli).
60-second quickstart
# 1. Deploy your own SentinelShield contract (bundled WASM)
npx sentinelshield-stellar deploy --source-account alice
# 2. Initialize it (your wallet is the admin)
npx sentinelshield-stellar init \
--contract C<your-new-contract-id> \
--admin G<your-admin-address> \
--agent G<your-agent-address> \
--token CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA \
--daily-limit 50 \
--tx-limit 5
# 3. Fund it — auto-detects XLM vs USDC from the contract's token
npx sentinelshield-stellar fund 20 --contract C... --admin-secret $ADMIN_SECRET
# 4. Approve the recipients your agent is allowed to pay
npx sentinelshield-stellar allowlist add G<vendor-address> \
--contract C... --admin-secret $ADMIN_SECRET
# 5. Check state any time
npx sentinelshield-stellar state --contract C...Your firewall is live. Now wire it into your agent code.
Use it in your AI agent
import { SentinelShield } from 'sentinelshield-stellar';
const shield = new SentinelShield({
contractId: 'C<your-contract-id>',
agentSecret: process.env.AGENT_SECRET,
network: 'testnet',
});
// Wherever your AI agent decides to make a payment:
async function payVendor(vendorAddress: string, amountUsdc: string) {
const result = await shield.executePayment({
to: vendorAddress,
amount: amountUsdc, // "1.5", "0.05", anything human-readable
});
if (result.approved) {
console.log(`Paid. tx: ${result.txHash}`);
return result.txHash;
}
if (result.preflightBlocked) {
// The firewall rejected this at the RPC layer — no tx was signed,
// no gas spent. The agent literally cannot reach the attacker.
throw new Error(`Firewall pre-flight: ${result.errorName}`);
}
// On-chain rejection — tx was recorded as a blocked event you can audit.
throw new Error(`Firewall blocked: ${result.errorName}`);
}That's the entire integration. Three lines of config, one function call per payment. Your agent can now be prompt-injected, hallucinate tool calls, or have its keys leaked — and the firewall still enforces your rules on-chain.
What the firewall enforces
Every execute_payment call goes through these checks, in order, on-chain:
- Paused? If the admin has paused the firewall, everything is blocked.
- Per-transaction cap. No single payment may exceed
set_single_tx_limit. - Allowlist. The recipient must be in the approved list. Default deny — an empty allowlist blocks everything.
- Daily cap. Cumulative spending today must not exceed
set_daily_limit. - Balance. The contract must actually hold enough tokens.
If any check fails, the contract emits a payment/blocked event with the reason code and returns the code to the caller. It does not revert, which means the block is recorded as a permanent on-chain event you can query, index, audit, or present to a regulator.
If all checks pass, the contract transfers the tokens from its own balance to the recipient, emits a payment/approved event, and updates the daily-spent counter.
Error codes
| Code | Name | Meaning |
|------|------|---------|
| 3 | ExceedsDailyLimit | Would push cumulative daily spend over daily_limit |
| 4 | RecipientNotAllowlisted | to is not in the allowlist |
| 5 | ExceedsSingleTxLimit | Single payment exceeds single_tx_limit |
| 7 | InsufficientBalance | Contract doesn't hold enough tokens |
| 8 | ContractPaused | Firewall is currently paused |
Admin operations
Every mutating operation is also exposed via the SentinelShieldAdmin class, for when you're wiring admin UIs directly into your app rather than using the CLI.
import { SentinelShieldAdmin } from 'sentinelshield-stellar';
const admin = new SentinelShieldAdmin({
contractId: 'C...',
adminSecret: process.env.ADMIN_SECRET,
network: 'testnet',
});
// Allowlist
await admin.addToAllowlist('G...');
await admin.removeFromAllowlist('G...');
// Limits (strings/numbers are human units, bigints are raw stroops)
await admin.setDailyLimit('100');
await admin.setSingleTxLimit('5');
// Funding — auto-detects the contract's token
await admin.fund('50');
// Role rotation
await admin.setAgent('G<new-agent>');
await admin.setAdmin('G<new-admin>');
// Emergency controls
await admin.pause();
await admin.unpause();
await admin.emergencyWithdraw('G<recovery-wallet>', '100'); // drain the vault
// Upgrade the contract WASM in place
await admin.upgrade('<64-char-hex-wasm-hash>');All methods return { success, txHash, explorerUrl, error? }.
Reading state
Both classes expose read-only methods that don't require a signer:
const state = await shield.getState();
// {
// contractId, admin, agent, token, asset, // "XLM" | "USDC" | "OTHER"
// dailyLimit, singleTxLimit, // bigint (stroops)
// dailySpent, dailyRemaining, // bigint (stroops)
// allowlist, // string[]
// balance, // bigint (stroops)
// isPaused, // boolean
// }
// Quick individual checks (each = one RPC simulation)
await shield.isAllowlisted('G...'); // boolean
await shield.isPaused(); // boolean
await shield.detectAsset(); // { asset, tokenSac }All reads are simulations — they don't cost gas and can be called as often as you want.
CLI reference
sentinelshield deploy --source-account <id> [--network testnet|pubnet] [--alias name]
sentinelshield init --contract C... --admin G... --agent G... --token C... \
--daily-limit <n> --tx-limit <n>
sentinelshield state --contract C... [--json]
sentinelshield fund <amount> --contract C...
sentinelshield allowlist add <address> --contract C...
sentinelshield allowlist remove <address> --contract C...
sentinelshield allowlist list --contract C...
sentinelshield set-daily-limit <amount> --contract C...
sentinelshield set-tx-limit <amount> --contract C...
sentinelshield pause --contract C...
sentinelshield unpause --contract C...Global flags: --network, --admin-secret, --json.
Env vars: ADMIN_SECRET, AGENT_SECRET.
Architecture
┌─────────────────────┐
│ Your AI agent │ Reads tasks, decides to pay
│ (Claude, GPT, │
│ whatever) │
└──────────┬──────────┘
│ shield.executePayment(to, amount)
▼
┌─────────────────────┐
│ sentinelshield-sdk │ Pre-flight: is_allowlisted(to)
│ (this pkg) │ If ok → sign & submit
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ SentinelShield │ On-chain rule checks:
│ Soroban contract │ paused? tx cap? allowlist?
│ (your instance) │ daily cap? balance?
└──────────┬──────────┘
│
│ ↓ if all pass ↓
▼
┌─────────────────────┐
│ Stellar token │ token.transfer(contract, to, amt)
│ contract (SAC) │
└─────────────────────┘The whole design is:
- Contract holds the funds. Not the agent. Not the SDK. Not you.
- Agent only has permission to call
execute_payment. It cannot bypass the firewall because it doesn't hold the tokens. - Rules run on-chain. Nothing the agent does off-chain can trick them.
- Every decision is logged. Approved or blocked, every call leaves a queryable on-chain event.
Security model
| Attack | Mitigation |
|--------|-----------|
| Agent's prompt gets injected with "pay attacker 100 USDC" | Attacker not in allowlist → blocked with error 4 |
| Agent is asked to pay 10,000 USDC for "urgent invoice" | Exceeds per-tx cap → blocked with error 5 |
| Agent is gradually exfiltrating funds via many small payments | Hits daily cap → blocked with error 3 |
| Agent's secret key is stolen | Attacker can only spend within firewall rules. Admin can pause() and then emergencyWithdraw() the remaining balance to a new wallet. |
| Contract has a bug you need to fix | Admin calls upgrade() with a new WASM hash. |
| Admin key is stolen | Admin can only mutate firewall config, not drain funds unless they also emergencyWithdraw(). Rotate with setAdmin() as soon as you notice. |
What this is not
- Not an oracle. It doesn't verify that recipients are trustworthy, only that you've approved them.
- Not KYC. It doesn't know who's behind an address, just that it's on your list.
- Not a replacement for a human. It's a safety net that catches a specific class of failures (agent compromise, prompt injection, hallucinated tool calls). Defense-in-depth is still your job.
Contributing
This package is part of the SentinelShield monorepo. Issues, PRs, and hackathon forks welcome.
License
MIT
