Skip to content

Agent

SDK reference · bounded execution

The agent is the “execute in the light” half of ShadowKit. It is deliberately bounded: the LLM only proposes amounts within the approved cap, and the on-chain AgentPolicy rejects anything that isn’t the exact approved action. The agent’s signing key lives only as a server/Worker secret — never in the browser.

PhaseModuleResponsibility
watchWatcherPoll RPC for ProposalClosed(id, approved=true) / status == Approved.
revealtally-revealtlock-decrypt the sealed votes and re-aggregate the tally for close.
dataDataClientx402-pay the premium-data endpoint; returns { pair, price, signal }.
planGeminiPlannerLLM decides amountIn / minOut (≤ cap) given the approved ActionSpec + market data.
signExecutorClient-side cap guard, then build + sign the swap with the session key.
submitAgentPolicyOn-chain policy authorizes the swap (gates); SwapVenue.swap executes; mark_executed (single-shot).
run-agent.ts
import { AgentRunner } from "@shadowkit/agent";
const runner = new AgentRunner({
rpcUrl, networkPassphrase,
govVaultId, agentPolicyId, swapVenueId,
sessionSecretKey, // ed25519 session key — server/Worker secret only
premiumDataUrl, // x402-protected endpoint the agent pays
geminiApiKey, // LLM key — server/Worker secret only
useDeterministicPlanner: false,
});
const { txHash } = await runner.run(proposalId, (log) => {
console.log(log.phase, log.message); // stream to the AgentBoard terminal
});

The primary planner is GeminiPlanner (model gemini-3.1-flash-lite via @google/genai streaming). It returns a JSON ActionPlan; the streamed reasoning deltas are emitted as AgentLog{phase:"plan"}. A DeterministicPlanner is the no-LLM fallback. The cap is enforced in three places — the prompt, the executor’s client-side guard, and the on-chain policy.

SymbolKindDescription
GeminiPlannerimplements PlannerPrimary LLM planner (@google/genai, streaming). Hard cap is in the system instruction AND re-validated by the caller.
DeterministicPlannerimplements PlannerNo-LLM fallback: amountIn = cap, minOut from market price − slippage.
Planner.plan(spec, cap, market)→ ActionPlan{ amountIn, minOut, reasoning } — bounded by cap.
AgentDepsinterfaceTest-injection seam: watcher, dataClient, govReader, executor, planner factories.

The Executor applies a defense-in-depth cap guard, builds the swap invocation, signs with the session key, and submits. The on-chain AgentPolicy then enforces its gates (approved · not-executed · target · asset · cap) plus the output-asset binding. After a successful swap, GovVault.mark_executed makes execution single-shot.

// inside the loop, after planning:
const { txHash } = await executor.executeSwap(plan, actionSpec, cap, proposalId);
// throws "client cap guard: amountIn ... exceeds cap ..." before signing if the plan is over cap;
// otherwise AgentPolicy.enforce validates it on-chain (PolicyError on any deviation).
ExportKindDescription
new AgentRunner(cfg, deps?)constructordeps is an optional injection seam; omit it in production.
run(proposalId, onLog){ txHash }Full loop, streaming AgentLog lines via onLog.
new Watcher({ rpcUrl, govVaultId })constructorSubscribes for the proposal-approved trigger.
new DataClient({ url, signerSecret, network })constructorx402-paying fetch over the premium-data endpoint.
new Executor({ rpcUrl, networkPassphrase, agentPolicyId, swapVenueId, sessionSecretKey })constructorBuilds, signs, and submits the policy-gated swap.
new LogBus()constructorTyped AgentLog emit/subscribe — the SSE/WebSocket source for the terminal.