Converts pay.sh catalog misses into funded builds: a failed catalog search is a willingness-to-pay signal, the studio builds against it, and the deliverable is a gated pay.sh endpoint the buyer's own agent then consumes.
Read in order: DESIGN.md (why it works),
ARCHITECTURE.md (system shape),
PLAN.md (milestones M0→M6),
GUIDELINES.md (how the studio builds anything: Rust-only
backends per the SF skills, AI-first project shape, agent registry,
skills.toml).
Invariant: the API holds no authoritative state. Every fact it serves is a projection of signed Nostr events or on-chain state; every state transition cites its evidence (event id or tx signature). The SQLite database is droppable and rebuildable from the substrates.
One binary — scarced (axum HTTP surface + orchestrator loop) — over seven
crates:
| Crate | Role |
|---|---|
studio-types |
wire types + generated JSON Schemas (the API contract) |
studio-core |
state machine + gate engine, pure functions (no I/O) |
studio-store |
SQLite projections (rebuildable by design) |
studio-buzz |
BuzzPort: Buzz-crate-backed impl + mock (M3) |
studio-pay |
PayPort: stub impl through M4, live MPP session impl in M5 |
studio-api |
axum routes, auth, SSE |
studio-registry |
agents/*.persona.md + roster.toml + skills.toml loader |
Implemented so far: RFQ capture (M1), quote issuance + gate-policy engine (M2), the Buzz lifecycle loop (M3: ops mirror, acceptance, private workrooms), and the agent registry loaded fail-closed at boot.
The registry (GUIDELINES.md §3–§4) is three in-repo artifacts: one
agents/<name>.persona.md per agent (Buzz persona format — YAML frontmatter
for settings, body is the system prompt), roster.toml for studio economics
(npub binding, skill tags, day rate) keyed by persona name, and skills.toml
pinning the convention skills personas reference by slug. scarced refuses
to start on any parse or cross-reference error.
just install scarce # cargo-installs the `scarced` binarycp scarced.example.yaml scarced.yaml # points at wss://scarce.communities.buzz.xyz
scarced --config scarced.yaml # or `just run --config scarced.yaml`Config precedence: defaults ← YAML ← SCARCED_* env (figment). Nested keys
join with __ in env form. --config is optional — env-only also works:
SCARCED_STUDIO_TOKEN=dev-token scarced| Key | Env | Default | |
|---|---|---|---|
bind |
SCARCED_BIND |
127.0.0.1:7380 |
HTTP bind address |
db |
SCARCED_DB |
sqlite://scarced.db |
projection store (droppable — rebuildable from substrates) |
studio_token |
SCARCED_STUDIO_TOKEN |
unset | bearer token for quote issuance; unset disables those routes (fail-closed) |
sweep_seconds |
SCARCED_SWEEP_SECONDS |
30 |
quote-expiry sweep cadence |
buzz.relay_url |
SCARCED_BUZZ__RELAY_URL |
unset | community relay the M3 orchestrator connects to |
The API is self-describing — start at the index:
curl -s localhost:7380/api/v1 | jq # every endpoint + schema links
curl -s localhost:7380/api/v1/schemas/rfq | jq # JSON Schema of any wire typeCapture demand (open, no auth — this is the signal intake):
RFQ_ID=$(curl -s localhost:7380/api/v1/rfqs --json '{
"query": "solana priority fee forecast api",
"buyer_npub": "npub1vadgs8qfwsgf7ak3jqvsys6dprae6eyyzzwr8v345l39yz77af4s7eg4zn"
}' | jq -r .id)Invalid input returns 422 with { "errors": [{ "field", "message" }] }.
Issue the quote (studio-authenticated; one per RFQ — a second POST is 409):
curl -s localhost:7380/api/v1/rfqs/$RFQ_ID/quote \
-H 'authorization: Bearer dev-token' --json '{
"price": { "amount": 250000000, "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" },
"milestones": [
{ "title": "Forecast model", "description": "p50/p90 per program id", "amount": 150000000 },
{ "title": "Gated endpoint", "description": "pay.sh-gated REST endpoint", "amount": 100000000 }
],
"timeline": "2 weeks, weekly demos",
"payout_destination": { "kind": "splits", "splits": [
{ "recipient": "CrewAgentA111111111111111111111111111111111", "bps": 10000 }
]},
"channel": { "idle_timeout_seconds": 604800 },
"expires_at": "2026-09-01T00:00:00Z"
}' | jqThe response carries the defaulted studio gate policy and its policy_hash
commitment. The buyer read is free: GET /api/v1/rfqs/$RFQ_ID/quote — status
is computed fail-closed against expires_at, so a lapsed quote reads
LAPSED even before the sweep stamps it.
Accept the quote (buyer, free, once — a second POST is 409, and a lapsed
quote refuses):
curl -s -X POST localhost:7380/api/v1/rfqs/$RFQ_ID/quote/accept | jq '{status, project_url}'Acceptance stands in for funding while payments are stubbed (PLAN.md §6
override path): the contract starts, and the response carries
project_url — the shareable page for the engagement.
{public_url}/project/{id} is a super-light web app embedded in the binary
(checked-in vanilla HTML/CSS/JS under web/, compiled in via include_dir —
no node toolchain). It renders GET /api/v1/projects/{id}: the public,
deliberately commercial-free view (title, state, milestone scope, timeline,
workroom name — never price, splits, budget, or policy) plus onboarding
links into Buzz. Set public_url in the config (e.g. https://scarce.sh)
to mint links against the deployed domain; unset, links use the bind
address for dev.
With the buzz config section present (see scarced.example.yaml), every
lifecycle beat is mirrored to the community relay: demand captured, quote
issued, and quote accepted post to the ops channel, and acceptance creates a
per-project workroom channel (proj-<slug>-<shortid>) where the
contract-starting post lands. The workroom's channel-create event id is
stored as the FUNDED → WORKROOM_ACTIVE evidence.
Channels are private: workrooms carry commercial terms, so only members
see them. The mirror adds the RFQ's buyer to the workroom on creation; the
studio identity is the channel owner. Operator one-offs (flip visibility,
add a member by npub) live in
cargo run -p studio-buzz --example channel_admin.
The daemon signs as the studio identity (buzz.private_key); a managed-agent
identity also needs the NIP-OA tag (buzz.auth_tag, env
SCARCED_BUZZ__AUTH_TAG). Omit the whole buzz section for a ledger-only
run.
just ci # fmt + clippy -D warnings + test — what CI runs
just schemas # regenerate schemas/*.json from studio-types (drift-tested in CI)
just --list # everything else