DEVELOPER GUIDEv0.1 · Experimental

A pricing decision.
Every layer of voxq.

Build the study, inspect the model’s assumptions, then put the questions in front of customers.

Meet Priya

Priya is a product engineer at a 14-person developer-tools company. The team is considering usage-based pricing and has ten days to investigate the risks. She wants to distinguish an objection to price from an objection to unpredictable bills.

This is an illustrative scenario. Her first two questions ask customers to explain their concerns; they do not estimate churn or willingness to pay.

Set up one study workspace

The terminal examples assume a repository checkout, Rust, Bun, jq, and an Ollama model you already have installed. Replace YOUR_INSTALLED_MODEL in the downloaded JSON before running inference. The examples do not download models or call paid providers automatically.

cargo build
export PATH="$PWD/target/debug:$PATH"
export VOXQ_DB="$PWD/pricing-study.sqlite"
export VOXQ_API="http://127.0.0.1:3102"
# Download the study JSON below and save it as pricing-study.json.
# Every curl below sends this token. Left unset, the server answers anyone who
# can reach the port; set to an empty string, it rejects every request.
export VOXQ_INTERNAL_TOKEN="$(openssl rand -hex 32)"
# Export the identical value in each terminal that issues curl requests.
# serve defaults to 0.0.0.0; bind loopback to keep the port on this machine.
voxq --db "$VOXQ_DB" serve --bind 127.0.0.1:3102

Download pricing-study.json. CLI, MCP, Scheme, and the private REST server can share this database. Choose one creation tab; the tabs are alternatives, so running all four creates four surveys.

The studio has its own account-owned workspace. A CLI survey is not automatically visible in the studio. For an end-to-end browser demo, use the studio quickstart and create the study there. Local studio responses are deterministic fixtures. The private REST examples below use your configured model.

1. Define the question bank

Two hypothetical personas probe different billing concerns. Their descriptions are assumptions for the model, not recruited or representative groups. Save the returned survey ID as SURVEY_ID for later requests.

jq '.questions' pricing-study.json > questions.json
jq '.personas' pricing-study.json > personas.json
jq '.bots' pricing-study.json > bots.json
voxq --db "$VOXQ_DB" create \
  --name "Pricing predictability" \
  --questions questions.json --personas personas.json --bots bots.json
# Read the returned ID, then:
export SURVEY_ID="YOUR_RETURNED_SURVEY_ID"
curl --fail-with-body -H "Authorization: Bearer $VOXQ_INTERNAL_TOKEN" \
  -X POST "$VOXQ_API/surveys" \
  -H 'Content-Type: application/json' \
  --data-binary @pricing-study.json
# Read .id in the response and set SURVEY_ID in your shell.
Tool: create_survey
Arguments: the contents of pricing-study.json
Replace YOUR_INSTALLED_MODEL before calling the tool.
Keep the returned id for subsequent tool calls.

The two questions are open-ended. See the MCP reference below
for the current structured-question schema limitation.
(define s (survey "Pricing predictability" "Explore variable-billing objections"))
(persona s "solo-builder" 25 45
  "An independent developer paying for tools personally. Usage varies with client work.")
(persona s "team-lead" 30 55
  "An engineering lead responsible for a team budget and explaining monthly costs.")
(bot s "local-model" "ollama" "YOUR_INSTALLED_MODEL")
(question s "Your plan changes from a flat fee to usage-based billing. What would you need to know before deciding whether to stay?")
(question s "Which change would make variable billing easier to manage, and why?")
(display s)

;; Save as pricing-study.scm, then run in your shell:
;; voxq --db "$VOXQ_DB" script pricing-study.scm
;; Keep the printed survey ID.

For CLI and REST JSON, the question field is type: open, multiple_choice, multi_select, ranking, dynamic, or scale. Choice questions need choices; scale questions need bounds. Stable question IDs connect the two sets of answers.

Add an answer-led discovery thread

Priya also wants to discover pricing concerns she did not anticipate. Add a dynamic root to the REST survey payload. Simulation answers the opening question only; real interviews can generate bounded follow-ups.

{"text":"Tell me about an unexpected billing experience.",
 "type":"dynamic", "required":false,
 "dynamic":{"goal":"Understand unexpected costs and their impact without assuming dissatisfaction.",
            "max_follow_ups":2}}

Follow-up SSE events use question_type: "open", with parent_question_id, a 1-based follow_up_index, and max_follow_ups. Submit the generated question ID through the existing answer endpoint. Skipping a follow-up ends that thread. GET/reconnect never generates a new question.

GET /surveys/{id}/interviews/analysis returns a separate discoveries array with parent ID, generated question ID, text, answer, and rationale. It excludes bearer session IDs. Generated answers do not enter fixed-question comparison. Semantic adherence to the goal is model-dependent; the API enforces shape, turn caps, and persisted provenance.

For multi_select and ranking, the answer field remains a string containing a JSON array of distinct authored options. Rankings must contain every option. Analysis retains per-respondent arrays as strings, not a pooled percentage.

{"question_id":"priorities", "answer":"[\"Predictability\",\"Price\",\"Flexibility\"]"}

The studio’s same-origin, signed-in POST /api/authoring/propose accepts {message, history, draft} and returns {message, questions, mode}. It proposes only: no survey is saved or overwritten. Local mode uses templates, connected mode uses Not Organic inference and records usage.

2. Check the estimated cost

Priya checks the cost before starting a run. An estimate uses assumed tokens per answer; it is not a spending cap. A missing model rate produces an unavailable estimate, so inspect provider pricing before proceeding.

# Estimation has no native command on this surface.
# Use the REST tab, or the studio on its own workspace.
# The REST server must use the same database as your CLI/MCP study.
curl --fail-with-body -H "Authorization: Bearer $VOXQ_INTERNAL_TOKEN" \
  -X POST "$VOXQ_API/surveys/$SURVEY_ID/estimate"
# Estimation has no native command on this surface.
# Use the REST tab, or the studio on its own workspace.
# The REST server must use the same database as your CLI/MCP study.
# Estimation has no native command on this surface.
# Use the REST tab, or the studio on its own workspace.
# The REST server must use the same database as your CLI/MCP study.

3. Run the simulation

One run asks both questions through both personas using the configured model: four attempted answers. Additional models increase that count. Rerunning appends responses; summaries combine saved runs.

voxq --db "$VOXQ_DB" run "$SURVEY_ID"
curl --fail-with-body -H "Authorization: Bearer $VOXQ_INTERNAL_TOKEN" \
  -X POST "$VOXQ_API/surveys/$SURVEY_ID/run"
Tool: run_survey
Arguments: {"survey_id": "YOUR_RETURNED_SURVEY_ID"}
;; Scheme defines surveys and reads saved results.
;; It has no native simulation runner.
;; After creating the survey, run from your shell:
voxq --db "$VOXQ_DB" run "$SURVEY_ID"

4. Inspect what the models said

Priya reads the individual explanations before drawing conclusions. If “budget predictability” appears repeatedly, it becomes a hypothesis to check with customers. It is not evidence that customers will stay.

voxq --db "$VOXQ_DB" results "$SURVEY_ID"
voxq --db "$VOXQ_DB" results "$SURVEY_ID" --raw
voxq --db "$VOXQ_DB" export "$SURVEY_ID" --output responses.json
curl --fail-with-body -H "Authorization: Bearer $VOXQ_INTERNAL_TOKEN" \
  "$VOXQ_API/surveys/$SURVEY_ID/results/raw"
curl --fail-with-body -H "Authorization: Bearer $VOXQ_INTERNAL_TOKEN" \
  "$VOXQ_API/surveys/$SURVEY_ID/analysis"
Tool: get_results
Arguments: {"survey_id": "YOUR_RETURNED_SURVEY_ID"}

Tool: get_analysis
Arguments: {"survey_id": "YOUR_RETURNED_SURVEY_ID"}
(display (get-results "YOUR_RETURNED_SURVEY_ID"))
(display (get-analysis "YOUR_RETURNED_SURVEY_ID"))

5. Open the survey to people

Open the same authored bank for human responses. Publishing permits new interview sessions and answers; closing blocks both. Surveys with attachments cannot currently be published for human interviews.

# Publishing has no native command on this surface.
# Use the REST tab, or the studio on its own workspace.
# The REST server must use the same database as your CLI/MCP study.
curl --fail-with-body -H "Authorization: Bearer $VOXQ_INTERNAL_TOKEN" \
  -X POST "$VOXQ_API/surveys/$SURVEY_ID/publish" \
  -H 'Content-Type: application/json' -d '{"accepting_interviews":true}'
# Publishing has no native command on this surface.
# Use the REST tab, or the studio on its own workspace.
# The REST server must use the same database as your CLI/MCP study.
# Publishing has no native command on this surface.
# Use the REST tab, or the studio on its own workspace.
# The REST server must use the same database as your CLI/MCP study.

For a study created in the studio, use Deploy → Open to respondents, then copy the interview link. Your own app, email list, or recruitment process supplies the participants.

6. Ask inside your product

The embed belongs on the product page where customers encounter the proposed change. The script host must be the Bun studio host serving the account-owned survey. It cannot be the private Rust listener or this marketing site.

# For a studio-owned study, copy the generated snippet from Deploy.
# The CLI does not publish a browser widget.
# Read the private API's sanitized survey configuration:
curl --fail-with-body -H "Authorization: Bearer $VOXQ_INTERNAL_TOKEN" \
  "$VOXQ_API/surveys/$SURVEY_ID/embed-config"
# For the standard widget, use the studio-owned survey and host below.
# Standalone REST studies need a server integration for browser access.
# There is no embed MCP tool.
# Use Deploy in the studio for the generated snippet.
;; There is no embed function in Scheme.
;; Use the studio's Deploy view for a studio-owned study.
<div data-voxq-survey="YOUR_STUDIO_SURVEY_ID"
     data-voxq-height="620"></div>
<script src="https://YOUR_VOXQ_HOST/embed.js" async></script>

Replace both placeholders using the studio’s generated snippet. The loader creates an iframe and validates resize messages. It does not send answers or interview session capabilities to the parent page. Read attributes, dynamic mounting, and browser troubleshooting, or configure your own domain.

7. Compare the answers

Priya reads the customer explanations beside simulated answers and identifies concerns the model missed. For choice and scale questions, the report aggregates each group with the same logic and displays separate sample sizes. Open answers remain text to review; there is no automatic semantic accuracy score.

# Human comparison has no native command on this surface.
# Use the REST tab, or the studio on its own workspace.
# The REST server must use the same database as your CLI/MCP study.
curl --fail-with-body -H "Authorization: Bearer $VOXQ_INTERNAL_TOKEN" \
  "$VOXQ_API/surveys/$SURVEY_ID/interviews/analysis"
# Human comparison has no native command on this surface.
# Use the REST tab, or the studio on its own workspace.
# The REST server must use the same database as your CLI/MCP study.
# Human comparison has no native command on this surface.
# Use the REST tab, or the studio on its own workspace.
# The REST server must use the same database as your CLI/MCP study.

The REST response contains predicted and human summaries. It includes saved answers from incomplete interviews and excludes failed simulations. Combined simulation runs and self-selected customers are different samples; agreement on this study does not validate another study.

Priya’s next decision: investigate whether a spending cap or a usage preview addresses the concerns people actually raised. Any example outcome here is illustrative, not a voxq benchmark or a customer result.

Population targets and reusable audiences

Priya can save the personas and target mix in Audience & models → Reusable audiences, then explicitly apply that audience to an unsaved study. The reducer replaces only personas and population settings. Questions, product context, and model choices survive. Human screeners are study-specific and are stripped from reusable audiences; copying a whole study remaps its screener to the fresh question ID.

The optional population field is included in the survey create payload. Give each persona exactly one target, use percentages summing to 100, and supply a reference or explicit assumption. No automatic normalization or learned calibration occurs.

{
  "population": {
    "reference": "Illustrative assumption: 70% individual buyers, 30% team leads.",
    "targets": [
      { "persona_label": "individual", "share": 70 },
      { "persona_label": "team_lead", "share": 30 }
    ],
    "human_screener": {
      "question_id": "AUTHORED_REQUIRED_SINGLE_CHOICE_ID",
      "choices": { "Just me": "individual", "My team": "team_lead" }
    }
  }
}

This is a payload fragment, not a complete study. Persona labels, screener ID, and choice keys must match the authored bank. Every screener option needs a mapping. The saved-audience API is the authenticated Bun surface GET / POST /api/audiences, scoped to the current workspace. Saving requires write permission and does not invoke inference.

GET /api/surveys/{id}/interviews/analysis adds population alongside the original comparison. Its human and predicted per-question summaries include raw and weighted values, segment coverage, and exclusions. A missing positive-target segment suppresses the weighted question result. Never renormalize the remaining segments or substitute zero. Human effective n is diagnostic, not statistical confidence. Open and dynamic answers are not weighted scalar measures.

Connect the studio to Not Organic

The studio uses a server-side PKCE connection with an HttpOnly browser-binding cookie. Its callback is /auth/notorganic. The app origin must be registered and mapped to the voxq product before live sign-in can succeed.

VOXQ_AUTH_MODE=live
VOXQ_PUBLIC_ORIGIN=https://APP_HOST_TO_BE_CHOSEN
VOXQ_MAX_COST_MICROUSD=OPERATOR_CHOSEN_INTEGER

These are configuration placeholders. The ceiling applies to each inference request. Credentials and rotating refresh tokens remain in server memory. A server restart requires sign-in again; multiple instances need shared encrypted credential storage and refresh coordination.

The studio reads the wallet and opens Not Organic for wallet management. Per-answer settlement remains unavailable until the gateway supplies a verifiable charge. The persona alias stays excluded from live requests until its upstream is enabled. Read the Not Organic integration contract.

Live voice through the owner’s gateway connection

Voxq uses an authenticated GET WebSocket upgrade to Not Organic’s /v1/live/sessions. This is the gateway’s Live path, not a Voxq-to-OpenAI key exchange or the older /v1/realtime route. No OpenAI key belongs in Voxq or its browser bundle. The Bun server uses the study owner’s server-held credentials for authorization, reservations, and owner-wallet billing.

# Operator configuration, not browser environment variables:
VOXQ_AUTH_MODE=live
VOXQ_LIVE_VOICE=true
VOXQ_VOICE_MAX_COST_MICROUSD=POSITIVE_OPERATOR_CHOSEN_INTEGER
VOXQ_VOICE_DAILY_CONNECTIONS=20

The voice ceiling is required and separate from the ordinary inference ceiling. The daily connection limit defaults to 20, is capped at 100, and uses a rolling 24-hour window. At most 3 connections are allowed per interview, including reconnects. The ceiling multiplied by the daily connection limit gives a budget bound, not an exact bill. Configuring a cap does not make respondent use free.

The owner must reconnect OAuth to grant realtime:connect when it is not already authorized, and separately accept Not Organic realtime logging consent. This provider consent is not the respondent’s microphone or local-recording consent. Deployment configuration alone does not enable a study: its Deploy toggle requires both can_write and can_spend. Disabling a study’s voice setting stops active voice connections.

The authenticated Bun route GET /api/surveys/{id}/voice-settings returns {enabled, configured, max_cost_microusd, daily_limit, calls, confirmed_seconds}. POST to the same route with {enabled: boolean}, then GET authoritative state again. These are product-backend routes, not private Rust API endpoints. A false configured disables the UI toggle and shows setup guidance.

Confirmed seconds may exclude unfinalized connections; actual settlement belongs to the Not Organic wallet, not a seconds-to-price calculation in Voxq. Live account, funded usage, and settlement proof remain pending. Credentials and active voice coordination currently require a single Bun instance; do not deploy horizontal replicas as though distributed ownership and refresh locking were implemented.

Voice, recording, and embed permissions

Keep typed controls available. Speech populates an editable answer but never submits it automatically. Microphone use is explicit opt-in. Recording requires an additional explicit local consent: the audio and timeline downloads stay on the respondent’s device, with no server recording storage or persisted voice transcript. Explicitly submitted survey answers remain normal persisted research data. Live voice still goes through Not Organic and OpenAI for remote processing.

Use HTTPS for the host and survey. The generated iframe must retain microphone and autoplay delegation; sandboxed embeds also need allow-downloads for local exports. The host’s Permissions-Policy and any ancestor frame policies must permit microphone use for the actual survey origin. Browser consent and playback restrictions still apply. Downloads belong in the sandbox permissions, not a fictitious downloads Permissions-Policy feature.

# Host HTTP response header, replace the example origin:
Permissions-Policy: microphone=(self "https://APP_HOST_TO_BE_CHOSEN"), autoplay=(self "https://APP_HOST_TO_BE_CHOSEN")

# Relevant attributes on the generated iframe:
allow="microphone; autoplay"
sandbox="allow-scripts allow-same-origin allow-forms allow-downloads"

These are permission examples, not a replacement for the loader or a complete deployment policy. Copy the app-generated embed and review the host’s existing policies instead of weakening them globally. Test microphone, audio playback, and explicit downloads on the published page; offer the full interview link when a site builder cannot delegate permissions.

CLI reference

Use voxq --help and voxq create --help for the installed binary. Keep the global --db flag before the command when sharing a study across surfaces.

voxq init --dir starter --name "Pricing study"
voxq --db "$VOXQ_DB" list
voxq --db "$VOXQ_DB" show "$SURVEY_ID"
voxq --db "$VOXQ_DB" run "$SURVEY_ID"
voxq --db "$VOXQ_DB" results "$SURVEY_ID" --raw
voxq --db "$VOXQ_DB" export "$SURVEY_ID" --output responses.json
voxq --db "$VOXQ_DB" script pricing-study.scm

delete SURVEY_ID removes the survey and its responses. There are no native estimate, publish, interview, or cost commands. Use REST for those operations.

REST reference

The private Rust API serves /surveys. The studio’s Bun backend serves browser requests at /api/surveys, enforces session ownership, and rewrites model aliases. These are different trust boundaries; keep the internal bearer token on servers and in local terminals.

Private Rust survey API
Operation Route
Create / list POST / GET /surveys
Read / delete GET / DELETE /surveys/{id}
Estimate / run POST /surveys/{id}/estimate · POST /surveys/{id}/run
Saved responses GET /surveys/{id}/results · GET /surveys/{id}/results/raw
Simulation analysis / cost GET /surveys/{id}/analysis · GET /surveys/{id}/cost
Open or close POST /surveys/{id}/publish
Public display configuration GET /surveys/{id}/embed-config
Start an interview POST /surveys/{id}/interviews
Session status / events GET /surveys/{id}/interviews/{session_id} · GET /surveys/{id}/interviews/{session_id}/stream
Submit an answer POST /surveys/{id}/interviews/{session_id}/answer
Compare groups GET /surveys/{id}/interviews/analysis
Research export GET /surveys/{id}/export (hosted: /api/surveys/{id}/export)

Research exports return a versioned JSON envelope with study details, answer/skip rows, comparisons, population reports, costs, and notes. The hosted route requires workspace membership and configured SSO, but no spending permission. The web UI prepares this once and encodes the same canonical records as CSV or JSON. Access tokens, external respondent IDs, and provider raw payloads are omitted. Respondent labels group answers within one export only. Collection may continue during preparation; this is not a transactional reporting cutoff.

Interview session IDs grant access to that session. Keep them out of analytics, parent-window messages, and public logs. Prefer the supplied interview client unless you need to implement session recovery, answer validation, and streaming yourself.

MCP reference

Configure a client to start the local binary over stdio. Replace both absolute paths with your checkout and study location; client configuration formats may vary.

{
  "mcpServers": {
    "voxq": {
      "command": "/absolute/path/to/voxq/target/debug/voxq",
      "args": [
        "--db",
        "/absolute/path/to/pricing-study.sqlite",
        "mcp"
      ]
    }
  }
}

Available tools: create_survey, list_surveys, get_survey, run_survey, get_results, get_analysis, delete_survey, and run_script. ID-based tools accept survey_id. Script execution accepts script.

The current advertised MCP schema calls the question field question_type, while deserialization reads type. This guide uses open questions to avoid that mismatch. Use REST for choice and scale studies and verify the saved question types. MCP creation also does not retain product context.

voxq --db "$VOXQ_DB" http-mcp --bind 127.0.0.1:8000

Scheme reference

Steel Scheme builds surveys declaratively and reads saved results. Its question helpers create open questions; use CLI or REST JSON for choice and scale. The persona helper takes five arguments after its name and does not set the separate context field.

(survey name description)
(question survey-id text)
(persona survey-id label min-age max-age description)
(bot survey-id name provider model)
(bot-at survey-id name provider model base-url key-env)
(vision-bot-at survey-id name provider model base-url key-env)
(question-with-image survey-id text image-src image-alt)
(question-with-doc survey-id text local-text-path filename)
(save-result survey-id bot-name persona-label question-id answer)
(get-survey survey-id)
(get-results survey-id)
(get-analysis survey-id)
(list-surveys)
(delete-survey survey-id)

Provider choices are openai, anthropic, ollama, and custom. Model availability comes from your provider configuration. Image questions require vision-capable bots; document resolution currently reads local text files. Attachments apply to simulation, not published human interviews.