CHARTER is a mandate and policy layer for AI trading agents on Binance. It is not a trading agent itself. Other agents' trade proposals have to pass through it before they can reach a Binance Agentic sub-account.
A human writes a covenant in plain English: spend caps, a symbol allowlist, leverage limits, a daily drawdown halt, a confirm-above-$X threshold. CHARTER compiles that into a live policy, simulates every proposal against real market data, and returns a real PASS, VETO, or ESCALATE verdict. Only a PASS, or a human-confirmed ESCALATE, ever reaches execution. Every step is written to a hash-chained audit log.
Built for the Binance Agent OS Mini Hackathon, Track A.
Binance's own coverage of the Agent OS launch names the problem this project addresses. TechCrunch's headline on the announcement: "Binance now lets AI agents trade, but keeping them in check is largely up to users." CHARTER takes that responsibility off the user and puts it in an enforced, auditable policy instead.
flowchart TD
Human["Human"] -->|"writes a covenant in plain English"| Mandate["Mandate\n(compiled policy: caps, allowlist, drawdown halt...)"]
Agent["Any agent\n(CLI, rogue-agent, or a third party)"] -->|"POST /propose"| API["CHARTER API"]
API --> Sim["Simulator\n(walks the live order book)"]
Mandate --> Engine["Policy engine"]
Sim --> Engine
Engine -->|"PASS"| Exec["Execution adapter"]
Engine -->|"ESCALATE"| Wait["Wait for human confirmation"]
Wait -->|"confirmed"| Exec
Engine -->|"VETO"| Blocked["No execution attempted"]
Exec --> Venue["Execution venue\n(testnet or mainnet MCP)"]
Venue --> Fill["Real fill"]
Fill --> Audit["Audit log\n(hash-chained, append-only)"]
Blocked --> Audit
A proposal only ever reaches a real exchange through the execution adapter, and the execution adapter only ever runs on a PASS or a confirmed ESCALATE. A VETO stops at the policy engine, which is why a vetoed proposal has no execution entry in the audit log at all, not a failed one, a missing one.
A verdict is never a bare PASS, VETO, or ESCALATE label. Every Verdict carries a reasons array with one entry per policy rule that ran, each shaped as { rule, outcome, detail } (outcome is "ok", "warning", or "violated"). This is the structured attribution behind the decision: which specific rule drove it, and why, not just the final outcome. It's produced by evaluateProposal in src/policy/engine.ts, which runs every rule and always returns the full result set regardless of decision; the shape itself is RuleResult in src/policy/types.ts.
Example, for a proposal that gets vetoed for exceeding the daily spend cap while every other rule passes:
{
"decision": "VETO",
"reasons": [
{ "rule": "symbolAllowlist", "outcome": "ok", "detail": "BTCUSDT is on the allowlist for BUY" },
{ "rule": "dailySpendCapUsd", "outcome": "violated", "detail": "Today's spend $490 + this proposal $15 exceeds dailySpendCapUsd $500" },
{ "rule": "maxSlippageBps", "outcome": "ok", "detail": "Projected slippage 2.1bps is within 50bps limit" }
]
}The CLI's propose command prints this array line by line (one of ✗ / ! / ✓ per rule), the API returns it verbatim in the POST /propose and GET /status/:id responses, and it's written to the audit log unmodified as part of every VERDICT_ISSUED entry, so audit tail and audit verify show the same attribution that decided the trade.
CHARTER always executes against a real order-matching engine. It never fabricates fills or simulation numbers. Which engine it uses depends on the EXECUTION_VENUE setting.
testnet is the default: Binance Spot Testnet, a real matching engine with virtual funds, at zero cost. This is what development and most of the demo footage run against.
mainnet-mcp points at the real Binance Agent OS MCP server, against a real, self-funded Agentic sub-account. It's used only where explicitly stated, with a small amount of real funds.
Every audit log entry records which venue produced it. Check data/audit.log.jsonl to see exactly which fills were testnet and which, if any, were mainnet.
npm install
cp .env.example .envGet testnet credentials by logging into https://testnet.binance.vision with GitHub, then fill in BINANCE_TESTNET_API_KEY and BINANCE_TESTNET_API_SECRET in .env.
npm run testnet:smokeThat command should print a real balance and a real live order book, proving the connection actually works before you go further.
npx tsx src/index.ts init
npx tsx src/index.ts propose BTCUSDT BUY --usd 15
npx tsx src/index.ts propose BTCUSDT BUY --usd 15 --execute
npx tsx src/index.ts mandate compile "Max 30 dollars per trade, spot only, halt at 8 percent drawdown"
npx tsx src/index.ts serve
npx tsx src/index.ts dashboard
npx tsx src/index.ts audit tail
npx tsx src/index.ts audit verifynpm run rogue-agent starts a separate process that submits a mix of compliant and violating proposals to a running charter serve instance, to demonstrate the veto working against a genuinely independent caller.
src/
config.ts env loading and validation
mandate/ covenant schema, NL to policy compiler, mandate storage
policy/ rule engine: evaluate(proposal, mandate, market) -> verdict
market/ public market data, order-book simulator, NAV calculation
venues/ ExecutionVenue interface, testnet and mainnet-mcp implementations
execution/ PASS verdict to real order placement
audit/ hash-chained, append-only audit log
api/ local HTTP API other agents call: POST /propose
cli/ CLI commands and the Ink terminal dashboard
rogue-agent/ separate demo process that proposes trades, some violating policy
venues/types.ts defines the ExecutionVenue interface. That abstraction is what makes moving from testnet to mainnet a config change rather than a rewrite.
MIT