Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.simplefunctions.dev/llms.txt

Use this file to discover all available pages before exploring further.

Status: draft RFC. This RFC defines the identity and governance rules that must be settled before SimpleFunctions implements @spfunctions/agent. It does not implement a package, endpoint, SDK method, Agent tool, or publish flow.

First principles

SimpleFunctions is not only a REST API, CLI, tool catalog, MCP adapter, or TypeScript SDK. The product direction is governed execution infrastructure for prediction-market, research, and trading-adjacent tools. Every serious tool call needs five answers:
  • who is calling
  • what exact canonical tool is being called
  • whether that tool is allowed
  • whether it costs money, consumes quota, mutates state, exposes secrets, starts runtime, or crosses execution boundaries
  • whether the call can be replayed and audited
The platform primitives are:
PrimitiveRole
IdentityAPI key, account, and future scoped session token
ContractGET /api/contracts/tools
Policypermissions, sideEffect, costEffect, and risk gates
ExecutionSDK resource call or Agent direct tool call
Tracedeterministic record, replay, and audit artifact
If any primitive is missing, an Agent SDK becomes either unsafe or not useful.

Decision

SimpleFunctions SDK and Agent SDK are API-key-first. For @spfunctions/sdk:
  • the constructor may keep apiKey?: string
  • docs and examples should pass apiKey: process.env.SF_API_KEY
  • no-key usage is allowed only for strict manifest inspection and explicitly allowlisted free public reads
  • methods whose contracts require identity must throw MissingApiKeyError when no key is configured
For @spfunctions/agent:
  • live execution requires SF_API_KEY by default
  • no-key mode is allowed only for replayOnly and static manifest inspection
  • replay misses must never fall through to live execution
  • anonymous live tool execution is forbidden
Security rules:
  • never embed a shared SimpleFunctions service key in SDK or Agent packages
  • never print, log, trace, or throw raw API keys
  • browser examples must not expose long-lived API keys
  • scoped browser or session tokens are a later design
When this RFC says “API key”, it means a SimpleFunctions API key issued to the user, account, or project. It never means a shared platform key bundled into npm packages.

Mode matrix

Surface or modeAPI key required?Allowed without key?Notes
sf.manifest.list()NoYesStrict contract bootstrap.
sf.manifest.get("world.read")NoYesContract inspection only.
SDK public free readUsually noOnly if explicitly allowlistedMust have sideEffect:none, costEffect:none, no user_data, and anonymousAllowed:true.
SDK auth-gated readYesNoThrows MissingApiKeyError.
SDK costly readYesNoSearch, LLM, venue, and upstream-cost surfaces need identity.
SDK user-specific readYesNoAnything with user_data requires key.
Agent SDK static manifest inspectionNoYesNo live execution.
Agent SDK replayOnlyNoYesReplay miss must not call live.
Agent SDK live call()YesNoRequired even for public tools.
Agent SDK live stream()YesNoSame rule as call().
Agent SDK v1 model-backed run()YesNoAlso requires budget and session policy.
Browser SDK with long-lived keyNoNoDefer scoped or short-lived tokens.
The key distinction is that SDK no-key public reads are a product concession, while Agent SDK no-key live calls are forbidden.

ContractTool additions

The strict manifest currently uses 0.2.0-draft. Adding identity and cost policy should move the semantic contract to:
0.3.0-draft
The required tool shape should include:
export type ToolStatus =
  | "implemented"
  | "deferred"
  | "deprecated"
  | "forbidden"

export type ToolStability =
  | "beta"
  | "experimental"
  | "internal"

export type SideEffect =
  | "none"
  | "cache_write"
  | "auth_telemetry_write"
  | "user_write"
  | "secret"
  | "runtime"
  | "paper_trade"
  | "live_trade"

export type CostEffect =
  | "none"
  | "api_cost"
  | "search_cost"
  | "venue_request_cost"
  | "llm_cost"

export type RiskTag =
  | "none"
  | "design_needed"
  | "hallucination_risk"
  | "secret_risk"
  | "execution_risk"
  | "trading_risk"
  | "deprecated"
  | "forbidden"

export type Permission =
  | "public_read"
  | "market_read"
  | "research"
  | "user_data"
  | "write"
  | "runtime"
  | "secret"
  | "paper_trade"
  | "live_trade"
  | string

export interface ContractToolAccess {
  anonymousAllowed: boolean
  anonymousReason?: string
}

export interface ContractToolHttpMapping {
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
  path: string
  query?: Record<string, unknown>
}

export interface ContractToolSdkMapping {
  package: "@spfunctions/sdk"
  method: string
  resource?: string
}

export interface ContractToolAgentMapping {
  callable: boolean
  defaultEnabled: boolean
  name: string
}

export interface ContractToolReplayPolicy {
  replayable: boolean
  match: "tool+inputHash"
}

export interface ContractTool {
  name: string
  status: ToolStatus
  stability: ToolStability
  authRequired: boolean
  permissions: Permission[]
  access: ContractToolAccess
  sideEffect: SideEffect
  costEffect: CostEffect
  risk: RiskTag[]
  schema: string
  http: ContractToolHttpMapping
  sdk?: ContractToolSdkMapping
  agent: ContractToolAgentMapping
  traceEvents: string[]
  replay: ContractToolReplayPolicy
}
access.anonymousAllowed is a positive allowlist. It prevents accidental anonymous execution if a tool is mistakenly marked authRequired:false, sideEffect:none, and costEffect:none.
function canRunWithoutKeyInSdk(tool: ContractTool): boolean {
  return (
    tool.access.anonymousAllowed === true &&
    tool.authRequired === false &&
    tool.costEffect === "none" &&
    tool.sideEffect === "none" &&
    !tool.permissions.includes("user_data")
  )
}

function requiresKeyInSdk(tool: ContractTool): boolean {
  return !canRunWithoutKeyInSdk(tool)
}

function agentLiveExecutionRequiresKey(): true {
  return true
}

sideEffect vs costEffect

llm_cost is not a sideEffect. It is a costEffect. sideEffect describes the tool’s product semantics:
  • state mutation
  • secret exposure or creation
  • runtime start or stop
  • paper or live trade execution
costEffect describes quota, upstream, search, venue, or LLM cost:
  • hosted API cost
  • search cost
  • venue request cost
  • LLM cost
A read-only LLM-backed answer should be:
{
  sideEffect: "none",
  costEffect: "llm_cost"
}
Do not mark every authenticated read as auth_telemetry_write merely because auth middleware updates key usage metadata. Contract sideEffect should describe the tool’s product semantics, not incidental platform telemetry. Use auth_telemetry_write only for a tool whose primary purpose is auth, session, or key telemetry mutation.

Policy ranking

Side-effect gates:
const SIDE_EFFECT_RANK: Record<SideEffect, number> = {
  none: 0,
  cache_write: 1,
  auth_telemetry_write: 1,
  user_write: 2,
  secret: 3,
  runtime: 4,
  paper_trade: 5,
  live_trade: 6,
}
Cost gates:
const COST_EFFECT_RANK: Record<CostEffect, number> = {
  none: 0,
  api_cost: 1,
  search_cost: 2,
  venue_request_cost: 2,
  llm_cost: 3,
}
costEffect is categorical. It is not dollar-budget enforcement. budgetUsd should remain a later feature until per-call cost estimates or response headers are reliable.

Initial annotation rules

Do not hand-classify from vibes. For every active strict contract tool, inspect whether it:
  • requires account or user context
  • reads user-specific data
  • calls paid or quota-limited upstream services
  • calls search
  • calls an LLM
  • hits venue or market-provider APIs
  • mutates user state
  • creates secrets
  • executes paper or live trade behavior
Default classification:
BehaviorsideEffectcostEffectanonymousAllowed
Strict manifest inspectionnonenonetrue
Explicit free cached public readnonenonetrue only if approved
Normal hosted API readnoneapi_costfalse
Search-backed readnonesearch_costfalse
Venue/provider-backed readnonevenue_request_costfalse
LLM-backed readnonellm_costfalse
User-specific readnoneapi_cost or higherfalse
Saved object writeuser_writedependsfalse
Secret/key/session mutationsecret or auth_telemetry_writedependsfalse
Runtime start/stopruntimedependsfalse
Paper tradepaper_tradevenue/request cost maybefalse
Live tradelive_tradevenue/request cost maybefalse and forbidden
Deferred or forbidden surfaces must remain out:
  • events.*
  • market.related as a semantic graph
  • investigations.create
  • intents.propose
  • webhooks.create
  • live_trade

SDK behavior

The SDK constructor can remain:
export interface SimpleFunctionsOptions {
  baseUrl?: string
  apiKey?: string
  userAgent?: string
}
Docs and examples should show:
import { SimpleFunctions } from "@spfunctions/sdk"

const sf = new SimpleFunctions({
  baseUrl: "https://simplefunctions.dev",
  apiKey: process.env.SF_API_KEY,
})
The SDK should add local identity helpers:
export class SimpleFunctions {
  hasApiKey(): boolean {
    return Boolean(this.apiKey)
  }

  getAuthState(): { hasApiKey: boolean } {
    return { hasApiKey: this.hasApiKey() }
  }
}
Do not implement auth.status for this. This is local SDK state only. SDK preflight should map each resource method to a canonical contract name:
async function preflightSdkCall(
  client: SimpleFunctions,
  toolName: string,
): Promise<void> {
  const tool = await client.manifest.get(toolName)

  if (!tool) {
    throw new UnknownToolError({
      tool: toolName,
      message: `Unknown canonical contract tool: ${toolName}`,
    })
  }

  if (!client.hasApiKey() && !canRunWithoutKeyInSdk(tool)) {
    throw new MissingApiKeyError({
      tool: tool.name,
      reason: missingKeyReason(tool),
    })
  }
}
The server must still enforce auth. SDK preflight is for developer experience and early failure, not security.

Agent SDK v0 behavior

@spfunctions/agent v0 is a governed direct tool runner. It is not:
  • a model-backed planner
  • a LangChain replacement
  • an MCP client
  • a CLI shell wrapper
  • a browser runtime
  • a live-trading agent
Implementation note: the v0 skeleton was created only after these prerequisites were in place:
  • ContractTool has costEffect
  • ContractTool has access.anonymousAllowed
  • every active strict tool has sideEffect and costEffect
  • SDK MissingApiKeyError exists
  • SDK manifest behavior is stable
  • Agent policy behavior has an approved test plan
Constructor sketch:
export interface AgentOptions {
  client: SimpleFunctions
  manifest?: ContractToolManifest
  policy?: AgentPolicy
  trace?: TraceStore
  mode?: "live" | "replayOnly" | "inspectOnly"
}

export interface AgentPolicy {
  allow?: Permission[]
  deny?: Permission[]
  maxSideEffect?: SideEffect
  maxCostEffect?: CostEffect
  requireAuthForUserData?: boolean
  budgetUsd?: number
}

export class SimpleFunctionsAgent {
  constructor(options: AgentOptions) {
    const mode = options.mode ?? "live"

    if (mode === "live" && !options.client.hasApiKey()) {
      throw new MissingApiKeyError({
        tool: "*",
        reason: "@spfunctions/agent live execution requires SF_API_KEY",
      })
    }
  }
}
Allowed without key:
new SimpleFunctionsAgent({
  client: new SimpleFunctions(),
  mode: "inspectOnly",
})

new SimpleFunctionsAgent({
  client: new SimpleFunctions(),
  mode: "replayOnly",
})
Forbidden without key:
new SimpleFunctionsAgent({
  client: new SimpleFunctions(),
})
Agent v0 must resolve only canonical dotted names from /api/contracts/tools. It must not resolve broad /api/tools names, MCP aliases, or deprecated legacy names.

Agent execution flow

Live execution flow:
  1. create runId and callId
  2. emit run.started
  3. load strict contract manifest
  4. resolve canonical tool
  5. emit tool.resolved
  6. reject non-implemented, deferred, deprecated, or forbidden tools
  7. reject agent.callable=false
  8. check API key
  9. evaluate policy
  10. emit policy.checked
  11. normalize input
  12. hash input
  13. validate input if schema is available
  14. emit tool.started
  15. execute through SDK/client contract executor
  16. redact output for trace safety
  17. write trace if configured
  18. emit tool.completed or tool.failed
  19. return ToolCallResult
Agent SDK must not shell out to CLI:
// forbidden
spawn("sf", ["agent", "--tool", toolName])
The CLI can later reuse Agent SDK internals, but Agent SDK must not depend on CLI.

Policy evaluation

Evaluation order should be deterministic:
  1. tool existence
  2. tool status
  3. agent.callable
  4. API key identity
  5. forbidden risk
  6. deny permissions
  7. allow permissions
  8. max sideEffect
  9. max costEffect
  10. user_data auth invariant
  11. future budget placeholder
Deny wins. If allow is omitted, side/cost gates still apply. If allow is provided, the tool permissions must be covered. live_trade is a hard stop in Agent v0 and must not be overrideable.

Typed errors

Shared SDK/Agent error codes:
export type SimpleFunctionsErrorCode =
  | "missing_api_key"
  | "invalid_api_key"
  | "permission_denied"
  | "unknown_tool"
  | "tool_not_callable"
  | "policy_denied"
  | "contract_invariant"
  | "invalid_input"
  | "replay_miss"
  | "tool_execution_failed"
  | "api_error"
Required errors:
export class MissingApiKeyError extends SimpleFunctionsError {
  code = "missing_api_key"
}

export class InvalidApiKeyError extends SimpleFunctionsError {
  code = "invalid_api_key"
}

export class PermissionDeniedError extends SimpleFunctionsError {
  code = "permission_denied"
}

export class UnknownToolError extends SimpleFunctionsError {
  code = "unknown_tool"
}

export class ToolNotCallableError extends SimpleFunctionsError {
  code = "tool_not_callable"
}

export class PolicyDeniedError extends SimpleFunctionsError {
  code = "policy_denied"
}

export class ReplayMissError extends SimpleFunctionsError {
  code = "replay_miss"
}

export class ContractInvariantError extends SimpleFunctionsError {
  code = "contract_invariant"
}
Errors must not include raw API keys, Authorization headers, secret values, or full sensitive payloads.

Trace and replay

Trace replay must be strict. Trace entry sketch:
export interface AgentTraceEntry {
  version: "0.1"
  ts: string
  sessionId?: string
  runId: string
  callId: string
  tool: string
  inputHash: string
  input: unknown
  output?: unknown
  outputSummary?: unknown
  error?: {
    code: string
    message: string
    retryable?: boolean
  }
  policy: {
    allowed: boolean
    matchedRules: string[]
  }
  sideEffect: SideEffect
  costEffect: CostEffect
  replayable: boolean
  redactions: string[]
  durationMs: number
}
Input normalization:
  • sort object keys recursively
  • remove undefined
  • preserve null
  • preserve array order
  • normalize Date to ISO string if present
  • never include API key or headers
Replay rule:
  • match by canonical tool plus normalized input hash
  • return recorded output on hit
  • throw ReplayMissError on miss
  • never silently call live when replay is requested

Agent events

sf agent --tool and @spfunctions/agent v0.stream() should produce compatible event concepts:
export type AgentEvent =
  | { type: "run.started"; ts: string; runId: string }
  | { type: "manifest.loaded"; ts: string; runId: string; schemaVersion: string; toolCount: number }
  | { type: "tool.resolved"; ts: string; runId: string; callId: string; tool: string; sideEffect: SideEffect; costEffect: CostEffect; authRequired: boolean }
  | { type: "auth.checked"; ts: string; runId: string; callId: string; hasApiKey: boolean; required: boolean }
  | { type: "policy.checked"; ts: string; runId: string; callId: string; allowed: boolean; reason?: string }
  | { type: "replay.hit"; ts: string; runId: string; callId: string; tool: string; inputHash: string }
  | { type: "replay.miss"; ts: string; runId: string; callId: string; tool: string; inputHash: string }
  | { type: "tool.started"; ts: string; runId: string; callId: string; tool: string; inputHash: string }
  | { type: "tool.completed"; ts: string; runId: string; callId: string; tool: string; durationMs: number; output?: unknown; outputSummary?: unknown }
  | { type: "tool.failed"; ts: string; runId: string; callId: string; tool: string; error: { code: string; message: string; retryable?: boolean } }
  | { type: "trace.recorded"; ts: string; runId: string; callId: string; traceId?: string }
  | { type: "run.completed"; ts: string; runId: string; durationMs: number }
  | { type: "run.failed"; ts: string; runId: string; error: { code: string; message: string } }
Compact mode should include policy-relevant metadata and avoid large schema blobs.

Browser key policy

Long-lived SimpleFunctions API keys must not be exposed in browser code. Allowed:
// server-side only
const sf = new SimpleFunctions({
  apiKey: process.env.SF_API_KEY,
})
Forbidden:
const sf = new SimpleFunctions({
  apiKey: "<long-lived-browser-exposed-key>",
})
Future browser support should use scoped or short-lived session tokens with restricted permissions, restricted tools, restricted cost effects, and possibly origin binding. Do not implement scoped browser tokens in this RFC.

Test plan

Contract tests:
  • /api/contracts/tools returns schema version 0.3.0-draft
  • every active implemented tool has sideEffect
  • every active implemented tool has costEffect
  • every active implemented tool has access.anonymousAllowed
  • anonymousAllowed=true implies authRequired=false, sideEffect=none, costEffect=none, and no user_data
  • user_data permission implies authRequired=true
  • live_trade is not active or callable
  • events.* is not active or callable
  • get_world_state is not canonical
  • get_regime_history is not canonical
SDK tests:
  • no-key SDK can call manifest.list()
  • no-key SDK can call manifest.get("world.read")
  • no-key SDK call to auth-required tool throws MissingApiKeyError
  • no-key SDK call to costEffect=llm_cost throws MissingApiKeyError
  • no-key SDK call to costEffect=search_cost throws MissingApiKeyError
  • no-key SDK call to side-effecting tool throws MissingApiKeyError
  • no-key SDK call to user_data tool throws MissingApiKeyError
  • no-key SDK call to an explicit free public read succeeds only when anonymousAllowed=true
  • errors never include raw API keys
Agent v0 acceptance tests:
  • live constructor without key throws MissingApiKeyError
  • inspectOnly constructor without key succeeds
  • replayOnly constructor without key succeeds
  • live call with key resolves world.read
  • live call rejects get_world_state
  • live call rejects get_regime_history
  • live call rejects events.search
  • live call rejects live_trade
  • Agent uses /api/contracts/tools, not /api/tools
  • policy deny list wins
  • max sideEffect blocks user_write
  • max costEffect blocks llm_cost when max is api_cost
  • replay miss throws ReplayMissError
  • replay miss does not call live
Docs tests:
  • no docs say @spfunctions/sdk is publicly published
  • no docs say @spfunctions/agent is publicly published
  • no docs say Agent SDK is the CLI
  • no docs say /api/tools is SDK/Agent truth
  • SDK examples use apiKey: process.env.SF_API_KEY
  • Agent SDK docs say live execution requires SF_API_KEY
  • browser docs do not show long-lived key usage

PR sequence

  1. RFC only: API-Key-First Identity and Governed Execution.
  2. Contract metadata taxonomy: add costEffect, access.anonymousAllowed, replay policy, and invariant tests.
  3. SDK identity, preflight, and typed errors.
  4. CLI parity metadata: compact events include costEffect; no semantic change.
  5. Private @spfunctions/agent v0 skeleton after metadata and SDK preflight are stable.
  6. Published docs sync so live docs match repo truth.

Hard stops

Do not:
  • publish @spfunctions/sdk
  • publish @spfunctions/agent
  • publish CLI without separate approval
  • run npm version
  • push main directly
  • create new endpoints
  • implement events.*
  • implement market.related
  • implement auth.status
  • implement investigations.create
  • implement intents.propose
  • implement webhooks.create
  • implement live_trade
  • treat /api/tools as SDK/Agent truth
  • shell out to CLI from Agent SDK
  • bundle a shared platform API key
  • show browser long-lived API key examples