Why split it
The split is the difference between a vendor and an HSM:- Custody stays with you. No bytes of secret material ever cross the API boundary. The same key model works whether you’re driving Privy, Phantom, a Turnkey policy, a TEE, or an env-loaded keypair.
- Idempotent retries. Prepare is a pure function of inputs and
network; calling it twice with the same
Idempotency-Keyreturns the same event id and transaction bytes. Submit is the only call that can land on chain, and it is also idempotent onevent_id. - Race-free reads. The
event_idexists before the transaction is broadcast, so you can persist it next to your domain object, re-read it, and never miss a state transition. The explorer andGET /v1/eventsboth join on it.
The prepare endpoints
Each one wraps aprepare* function from @leash/registry-utils,
returns the wire-encoded transaction, and creates a prepared event:
| Endpoint | SDK function | What it does |
|---|---|---|
POST /v1/agents/prepare | prepareAgentMint | Mint a brand-new agent (Core asset + on-chain identity). |
POST /v1/agents/{mint}/identity/prepare | prepareRegisterAgentIdentity | Re-register identity on an existing asset. |
POST /v1/agents/{mint}/executive/register/prepare | prepareRegisterExecutive | Bind a wallet as an executive for this agent. |
POST /v1/agents/{mint}/executive/delegate/prepare | prepareDelegateExecution | Let the executive sign Execute for this asset. |
POST /v1/agents/{mint}/delegation/prepare | prepareSetSpendDelegation | Owner approves the executive as SPL delegate up to a cap. |
POST /v1/agents/{mint}/delegation/revoke/prepare | prepareRevokeSpendDelegation | Drop the SPL delegate to zero. |
POST /v1/agents/{mint}/token/set/prepare | prepareSetAgentToken | Set the agent_token field on the identity plugin. |
POST /v1/agents/{mint}/treasury/provision/prepare | prepareProvisionTreasuryAtas | Pre-create stable ATAs on the treasury PDA. |
POST /v1/agents/{mint}/treasury/withdraw/prepare | prepareWithdrawTreasury | Owner moves a specific SPL amount out of the treasury. |
POST /v1/agents/{mint}/treasury/withdraw-all/prepare | prepareWithdrawTreasuryAll | Owner drains the full SPL balance (or no_op when zero). |
POST /v1/agents/{mint}/treasury/withdraw-sol/prepare | prepareWithdrawTreasurySol | Owner moves lamports out of the treasury. |
POST /v1/agents/{mint}/treasury/withdraw-sol-all/prepare | prepareWithdrawTreasurySolAll | Owner drains all SOL above a configurable safety reserve. |
POST /v1/buyer/payment/prepare | (buyer-kit prepareTransfer) | Build an unsigned SPL TransferChecked for a buyer x402 settlement. |
POST /v1/agents/prepare is the HTTP twin of
prepareAgentMint: it calls the Metaplex Agents
API under the hood, returns the unsigned transaction in the same
prepared envelope as every other route, and pre-registers the asset +
treasury PDA on the indexer watchlist so the explorer lights up the
moment the signed tx lands. The SDK helper remains available for
TypeScript callers that want to skip the HTTP hop.
Indexer follow-up after your first agent action.
POST /v1/agents/prepare and every /v1/agents/{mint}/... prepare
endpoint auto-register the agent asset + treasury PDA on the
indexer watchlist (via ensureWatched). That’s enough for the
explorer to surface mints, withdraws, identity changes,
delegations, and token updates.It is not enough to surface incoming SPL deposits, because plain
TransferChecked transactions don’t list the PDA — they list the
ATA. To make agent.treasury.fund events show up too, do one
of the following exactly once per agent:- Hit
POST /v1/agents/{mint}/treasury/provision/prepare(registers the ATAs it provisions), or - Hit
POST /v1/agents/{mint}/treasury/withdraw/prepare(registers the source ATA), or - Hit
GET /v1/agents/{mint}/treasury/balances— read-only, free, and registers the PDA + every SPL ATA it sees in one call.
/treasury/balances call is the recommended “make this agent
fully visible to the explorer” step right after your first
successful submit. See Explorer tracking
for the full per-event matrix.signers is the ordered set of pubkeys the API expects on the
message_base64 you return to /v1/submit. base64 is the same
transaction with empty signature slots — most polyglot SDKs sign
that directly with signTransaction(tx, [signer]) and POST the
resulting bytes back. fee_payer is the first account in the
message and is always the first entry of signers.
A small subset of helpers can short-circuit when there’s nothing to
do — provision-when-already-provisioned, withdraw-all-when-empty.
They return a “no-op” envelope with a null event id and no
transaction:
The submit endpoint
- Validates the signed tx parses, blockhash is fresh, and
event_idis inphase = prepared(not already submitted). - Broadcasts via the API’s RPC pool.
- Stamps the event to
phase = submittedwith the returned signature. - Returns
200 OKwith{ event_id, signature, phase: "submitted", network }. Confirmation is a separate poll.
getSignatureStatuses for every
submitted event and flips it to confirmed (with confirmed_at,
slot, block_time) or failed (with error_code, error_logs).
Tracking the result
https://explorer.leash.market/tx/<signature> (devnet or mainnet
depending on the API key prefix that produced it).
Worked example: set a USDC delegation
- Monetise an existing API — plug this lifecycle into a SaaS that already exists.
- Explorer tracking — the per-event-kind
cookbook: which
prepare → submitpair (or which on-chain trigger) makes each row show up on the explorer.

