Tokenized equities playbook¶
Status: Live (jurisdiction-gated)
Tokenized equities are the most tightly gated venue on the platform:
eligibility is checked per wallet and per jurisdiction, and ineligible
regions are refused at transaction-build time, not after. This playbook
walks the full flow end to end, grounded directly in
crank_mcp/tools/equity_tools.py: jurisdiction attestation, classification,
quoting, the mandatory confirmation gate, execution, post-trade
verification, and market-hours handling.
Every tokenized equity is a SPL token wrapping exposure to an underlying security; trading it is a Jupiter-routed spot swap under equity-specific guardrails -- there is no separate "equity engine."
1. Discover what is listed¶
markets = await client.call_tool("equity_markets", {})
# -> {"count": N, "markets": [...], "market_hours": {...}, "disclaimer": "..."}
Each market row carries symbol, name, mint, issuer,
regulatory_framework, plus best-effort live price_usd /
volume_24h_usd / market_cap_usd (degrades to null if market data is
unavailable -- never fails the read).
2. Check classification before quoting¶
Read-only, resolves a mint or symbol against the token registry and returns
every classification dimension (backing, liquidity tier, issuer
verification, market-data quality) plus a derived composite risk gate.
This is fail-safe by design: a registered-but-not-yet-scored token reads as
blocked until the daily reclassification task verifies it, and an
unregistered token is always blocked -- nothing is implicitly tradeable.
A blocked composite risk refuses trade_equity outright later in the
flow; check this first so your agent does not build toward a dead end.
3. Jurisdiction attestation (Reg S)¶
Tokenized equities on Crank run under a Reg S framework: no US persons, and an unknown jurisdiction is DENIED fail-safe (absence of proof is not permission). Declare once, standalone of any trade:
await client.call_tool("declare_jurisdiction", {
"wallet_address": "<AGENT_PUBLIC_KEY>",
"jurisdiction": "SG", # ISO-3166-1 alpha-2, self-declared, never inferred
})
The attestation persists 90 days and is re-used by every gated call
(trade_equity, perps, shorting) unless you pass jurisdiction inline on
that call instead. trade_equity also accepts jurisdiction directly, and
ip is checked as an additional Reg S signal (an OFAC SDN screen runs on
the wallet address in both cases) -- neither the jurisdiction code nor the
IP is ever inferred or offered as advice (hard rules 2, 5-8).
4. Market hours (24/7 on Solana, NYSE-aware)¶
Tokenized equities trade 24/7 on Solana -- the structural advantage over
the underlying's NYSE session. This tool returns the NYSE session status
(open/closed, next open/close) purely as context; equity_quote and
trade_equity both still succeed with the market "closed" (they attach an
informational market_closed disclosure rather than refusing the trade).
5. Quote first (free, non-binding)¶
quote = await client.call_tool("equity_quote", {
"symbol": "AAPLX",
"amount": 500_000_000, # USDC base units (6dp) on a buy
"side": "buy",
"slippage_bps": 50,
})
Returns expected output, price impact, the effective technology-service-fee
breakdown (fee_model, flat for equities -- no staker/pay-in-$CRANK
discount affordances on this rail, crypto-only per the fee schedule), NYSE
market-hours context, and the standard equity disclaimer. This is a
non-binding preview -- no execution, no confirmation gate.
6. Trade: the three-call confirmation gate¶
trade_equity enforces no autonomous equity execution -- every trade
needs an explicit human/agent confirmation step before anything is built.
Call 1 -- preview (no confirm):
preview = await client.call_tool("trade_equity", {
"symbol": "AAPLX",
"side": "buy",
"amount": 500_000_000,
"wallet_address": "<AGENT_PUBLIC_KEY>",
})
# -> {"action": "confirmation_required", "geo": {...}, "risk_warnings": [...], ...}
Runs the full guardrail chain before returning anything: classification gate
(blocked composite risk raises CLASSIFICATION_BLOCKED), Reg S geo-gate +
OFAC screen (raises GEO_RESTRICTED), token-authenticity verification
including the issuer's published metadata authority (an impersonated equity
mint is blocked outright), and a price-impact guard (raises
PRICE_IMPACT_EXCEEDED above EQUITY_MAX_PRICE_IMPACT_PCT). Without
confirm=true it stops here with action: "confirmation_required".
Call 2 -- build (confirm=true, no signed_transaction):
build = await client.call_tool("trade_equity", {
"symbol": "AAPLX",
"side": "buy",
"amount": 500_000_000,
"wallet_address": "<AGENT_PUBLIC_KEY>",
"confirm": True,
})
# -> {"action": "sign_required", "transaction": "<base64>", ...}
Same guardrail chain re-runs, then returns an unsigned transaction for your agent's own wallet to sign. Crank never signs (hard rule 1).
Call 3 -- execute (signed_transaction set):
executed = await client.call_tool("trade_equity", {
"symbol": "AAPLX",
"side": "buy",
"amount": 500_000_000,
"wallet_address": "<AGENT_PUBLIC_KEY>",
"confirm": True,
"signed_transaction": "<BASE64_SIGNED_TX>",
"verify": True,
})
# -> {"action": "executed", "tx_signature": "...", "verification": {...}}
Broadcasts the caller-signed bytes and records the fill. With verify=True
(default), Crank awaits on-chain confirmation and re-reads the equity
mint's post-trade balance, returned as verification -- gate any follow-on
sizing decision on verification.confirmed, never on tx_signature
presence alone. Set verify=False to skip the wait for latency-sensitive
callers (the trade still broadcasts).
| Param | Type | Default | Meaning |
|---|---|---|---|
symbol |
string | required | Tokenized-equity ticker (e.g. AAPLX). |
side |
string | required | "buy" or "sell". |
amount |
int | required | Base units of the INPUT token: USDC (6dp) on a buy, the equity token on a sell. |
wallet_address |
string | required | Agent's managed wallet address. |
jurisdiction |
string | none | Inline attestation instead of a prior declare_jurisdiction call. |
confirm |
bool | false |
Must be true to build a transaction -- no autonomous execution. |
slippage_bps |
int | 50 |
Max slippage. |
signed_transaction |
string | none | Set on the execute call. |
allow_unverified |
bool | false |
Bypass token-authenticity verification (not recommended). |
ip |
string | "" |
Caller origin IP, an additional Reg S signal. |
venue_hint |
string | none | Advisory only -- equity routes via the issuer registry, one surface today. |
verify |
bool | true |
Await on-chain confirmation + balance re-read on execute. |
7. Track positions¶
Filters the wallet's balances to registered equity mints with live USD
pricing, issuer/regulatory metadata, and NYSE market-hours context.
avg_cost and unrealized_pnl are null today -- cost-basis tracking is a
documented follow-up (derivable from recorded swap history), not silently
fabricated.
8. Corporate events (the after-hours-agent case)¶
Earnings, dividend, and split calendar for the underlying -- the events an overnight strategy reacts to while the tokenized wrapper trades 24/7 on Solana regardless of the NYSE clock. Returns an honest empty payload plus a SEC EDGAR link when no data feed is configured, never a fabricated calendar.
Mechanical automation: Equity DCA¶
To automate step 6 on a recurring schedule instead of driving it by hand,
see the Equity DCA strategy guide -- DCA is
classified mechanical (not discretionary), so it stays autonomous on an
equity target even though trade_equity itself requires per-call
confirmation; the strategy's config is what the owner already confirmed at
creation time.