Version 0.1.0, updated 2026-09-15 after review. Download the source (tgz) A public repository follows; until then this tarball is the release. Tests: 12 passing with node --test.
fifty-guard
Open-source controls for AI agents that handle money on Solana. fifty-guard checks a proposed transaction against a policy file and returns a verdict: allowed or rejected, with reasons. Nothing here ever signs a transaction.
Honest positioning, v0.1: this is a transaction policy linter, not an enforcement boundary. A caller can ignore the verdict, edit the policy, or sign a different transaction. The verdict becomes a control only when the signing path requires an approval artifact bound to the exact transaction hash and policy hash; that is roadmap item one. Until then, treat it as a preflight check and a public record, not a guarantee.
FIFTY (the memecoin project this was built alongside) is the live test case: an AI agent given a small treasury. From 2026-09-15 on, every proposed dev-wallet move is run through this check and the audit line is published; moves before that date were not, and the tool did not exist yet.
What this is not
- Not an enforcement boundary yet. See the positioning note above.
- Not a signer. There is no code path in this package that produces a
signature or touches a private key. It only reads and reports.
- Not a way to make a token valuable. It is a safety control, unrelated to
price, marketing, or any token's fundamentals.
- Not tied to owning FIFTY, or any token. It works standalone.
Install
npm install fifty-guard
Or clone this directory and run npm install (zero runtime dependencies beyond @solana/web3.js).
CLI usage
fifty-guard check --tx <base64|path> --policy policy.json [--rpc URL] [--audit audit.jsonl]
fifty-guard verify --audit audit.jsonl
fifty-guard policy-hash --policy policy.json
checkdeserializes the transaction (legacy or v0), evaluates it against
the policy, prints a human-readable verdict, and exits 0 when allowed, 2 when rejected.
--txaccepts a base64 string directly or a path to a file containing one.--rpcis optional. When set, the transaction is also run through
simulateTransaction to catch anything that would fail on-chain.
--auditis optional. When set, every check is appended to that file as
one hash-chained JSON line (see below).
verifychecks an audit file's hash chain (locally tamper-evident: it detects edits inside the file you are shown, not omitted evaluations or a replaced file; anchoring the latest hash somewhere independent is roadmap item two) for tampering.policy-hashprints the sha256 of a policy file's canonical JSON, so a
policy that's published somewhere can later be proven unchanged.
Library usage
import { evaluate } from 'fifty-guard/src/evaluate.js';
const result = await evaluate(txBase64, policy, { ledgerPath: './ledger.json' });
// result.allowed, result.rule, result.reasons, result.summary, ...
Policy fields (policy.example.json)
| Field | Meaning |
|---|---|
maxSolPerTx | Maximum SOL a single transaction may move out of the signer's account. A transaction over this is rejected. |
maxSolPerDay | Maximum SOL total across all transactions approved in a UTC day, tracked in the ledger file. A transaction that would push the day's total over this is rejected, even if it is under maxSolPerTx on its own. |
allowedPrograms | Program IDs the transaction is allowed to invoke. Any instruction targeting a program not on this list rejects the whole transaction. |
allowedDestinations | Account addresses the transaction is allowed to send funds or touch as a non-signer account. The signer's own address and the fee payer are always implicitly allowed. |
requireHumanApproval | When true (the default if omitted), every verdict carries needsHuman: true. This is a reporting flag only, since fifty-guard never signs regardless of this setting. |
signer | The public key of the wallet whose outgoing balance is being controlled. Used to identify which side of a transfer is "out". |
Audit chain
--audit audit.jsonl appends one JSON object per line. Each line includes prevHash (the previous line's hash, or null for the first line) and its own hash (a sha256 of everything else in that line). This means:
- Anyone can run
fifty-guard verify --audit audit.jsonland confirm no line
was edited or deleted after the fact.
- Editing any line changes its hash, which breaks every
prevHashafter it,
so verify reports the first index where the chain no longer matches.
policyHash(policy)lets a policy published publicly (e.g. in a repo or a
tweet) be checked against the actual policy file in use, without exposing the policy's private fields if you choose to hash it before adding secrets.
This is a local, offline chain (no blockchain, no external service). It proves internal consistency of the log, not that the log itself wasn't replaced wholesale; publish the file or its running hash somewhere append-only (e.g. commit it) if you need that guarantee too.
Limitations
1. **SPL token transfers are listed as programs and destinations, but token amounts are not valued in SOL in this version.** A transaction that moves SPL tokens will show the token program and the destination token account under policy checks (so allowedPrograms / allowedDestinations still apply), but maxSolPerTx / maxSolPerDay only count native SOL moved by SystemProgram.transfer. A large token transfer through an allowed program to an allowed destination will not be capped by SOL value. 2. **CPIs (cross-program invocations) inside an allowed program are not inspected.** fifty-guard only reads the top-level instruction list from the transaction message. If an allowed program internally calls another program via a CPI, that inner call is invisible to this check. 3. The SOL-out estimate is an instruction sum, not a balance diff. simulateTransaction's account snapshot only returns a post-run balance with no pre-run balance in the same response, so --rpc improves correctness by catching failing transactions, not by refining the amount.
Roadmap
- Value SPL token transfers against
maxSolPerTx/maxSolPerDayusing a
price oracle, so token moves are capped the same way SOL moves are.
- Walk known CPI patterns (e.g. Token program calls inside a swap program) so
an allowed outer program can't smuggle an unlisted inner transfer.
- A pre-run balance diff via a real simulation flow (fetch balance, simulate,
fetch balance) instead of the instruction-sum estimate, when --rpc is set.
Disclosure
This is a safety tool, not a guarantee. It reduces the ways a signing key can be misused by an automated agent; it does not replace reviewing what you sign, and it cannot protect funds once a transaction has already been signed and sent outside of it.
License
MIT
Try it and tell me what breaks
Three unrelated developers testing this is the week-three goal. If you run it, send what blocked you through the Operator Inbox or ops@fiftycoin.lol. Feedback is published only with permission.