RxRelay

RxRelay

Enables consent-first voice coordination for prescription access with a deterministic proof gate, exposing MCP tools to manage cases, attestations, and evidence.

Category
Visit Server

README

<div align="center">

<img src="assets/social-preview.png" alt="RxRelay — Make the calls. Bring the proof." width="100%" />

RxRelay

A voice agent that has to prove it helped.

Consent-first voice coordination for prescription access.
A case can close only when consent ∧ action ∧ counterpart outcome ∧ patient update are on the record — not when an LLM says “done.”

Website Pitch Deck License: MIT Node ≥20 Runtime deps

CI Tests PAVO Live telephony

Website · Judge demo · Pitch deck · PPTX · Quickstart · Proof gate · Paper

</div>


✅ Product completeness (v0.2)

Everything judges need for demo + evaluation is shipped and tested.

Surface Status
Consent + deterministic 4/4 proof gate Works (tested)
Pharmacy→clinic→ready→SMS proof path Works (sandbox E2E; voice + dashboard)
Inbound TeXML voice (voice-server.mjs) Works (isolated blast radius)
PAVO demand routing + safe-stop Works; verified turns upgrade speech+DTMF + strong model
Signed hash-chained proof receipts Works (/api/cases/:id/receipt)
Counterpart attestation (pharmacy/clinic) Works (/attest/:token — attestation seam without EHR)
Human ops queue + resume + timeout scan Works
Live SSE proof stream Works (/api/events)
MCP tools (8) Works
Marketing site + 13-slide HTML/PPTX deck Shipped
Outbound SMS + coordination call adapters Works — sandbox always; live carrier path is intentionally fail-closed until OTP allowlist + ALLOW_LIVE_TELEPHONY / call-action URL (safety, not a missing feature)

📖 Table of contents

Demo number (live inbound): +1 (802) 676-8127 · full judge script: docs/JUDGE_DEMO.md


⚡ Why this exists

A prescription can be clinically approved and still be unreachable. Something stalls — prior auth, stock, a missing form — and the patient becomes the switchboard:

call pharmacy → call clinic → call insurer → repeat context → still no trustworthy answer.

Voice agents are an obvious fit for that loop. The failure mode is subtler:

An agent that says “I’ve taken care of it” and an agent that actually coordinated something look identical at the transcript layer.

In medication access, that gap is the whole risk.

RxRelay closes the gap by refusing to close a case it cannot substantiate.

Generic voice agent RxRelay
“I’ll take care of it.” “Here is the evidence I can prove.”
One inference path for every turn PAVO-style routing across ASR and reasoning
Conversation ends ⇒ task done Case stays open until the proof gate is satisfied
Treats every request as automatable Hard-stops clinical advice, Rx changes, emergency cues, controlled-inventory questions
Failed API call still narrates success Failed provider call records no action evidence

🔐 The proof gate

This is the core idea, and it is deliberately boring: an LLM never decides that a case is resolved. A pure function over recorded state does.

// src/store.mjs — the close gate is not generative
function resolutionProof(caseRecord) {
  const checks = [
    { id: "consent",      label: "Explicit consent recorded",               passed: caseRecord.evidence.consentRecorded },
    { id: "action",       label: "Permitted coordination action completed", passed: caseRecord.evidence.permittedActionCompleted },
    { id: "outcome",      label: "Counterpart outcome recorded",            passed: caseRecord.evidence.counterpartOutcomeRecorded },
    { id: "notification", label: "Patient notification sent",               passed: caseRecord.evidence.patientNotificationSent },
  ];
  return { checks, ready: checks.every((check) => check.passed) };
}
 consent  ∧  permitted action  ∧  counterpart outcome  ∧  patient update
 ───────     ────────────────     ──────────────────     ──────────────
 recorded    provider-accepted    pharmacy/clinic fact   consented SMS
                 (or sandbox)
                              │
                              ▼
                     Resolution verified
                   (every other state stays open)
Property How it is enforced
No action before consent requireConsent() throws on every coordination and messaging path
No fabricated completions Evidence flags are set by state transitions, never by model output
Failure is visible A rejected provider call leaves permittedActionCompleted === false
Partial progress stays open Clinic submission ≠ resolved Rx; pharmacy confirmation is still required

There is a dedicated honesty test — a provider that throws must not manufacture action evidence:

test("failed outbound coordination cannot create a false action proof", async () => {
  const failingTelephony = { placeCoordinationCall: async () => { throw new Error("Provider unavailable"); } };
  const store = new CaseStore({ telephony: failingTelephony });
  await assert.rejects(() => store.beginCoordination("RX-1048"), /Provider unavailable/);
  assert.equal(store.get("RX-1048").evidence.permittedActionCompleted, false);
});

🖥️ Screenshots

Proof board Architecture PAVO routing
<img src="assets/proof-board.png" alt="Proof board" width="100%" /> <img src="assets/architecture.png" alt="Architecture" width="100%" /> <img src="assets/pavo-routing.png" alt="PAVO routing" width="100%" />
Deterministic close gate End-to-end system topology Demand-conditioned pipelines
Problem framing Live demo flow Safety contract
<img src="assets/problem.png" alt="Problem" width="100%" /> <img src="assets/demo-flow.png" alt="Demo flow" width="100%" /> <img src="assets/safety.png" alt="Safety" width="100%" />

Full narrative deck: HTML · PPTX (npm run deck)


🏗 Architecture

Two processes. One shared case file. The public tunnel only ever touches the TeXML voice gateway — never the dashboard or MCP surface.

  Caller (consented)
        │
        ▼
 ┌──────────────────────────────────────────┐
 │  Cloudflare quick tunnel                 │
 │  (scripts/live-inbound.mjs)              │
 └────────────────────┬─────────────────────┘
                      │ TeXML only
                      ▼
 ┌──────────────────────────────────────────┐     ┌──────────────────────────────────────────┐
 │  voice-server.mjs :3001                  │     │  server.mjs :3000                         │
 │  /voice  /voice/turn  /health            │     │  proof board · /api/cases · /mcp          │
 │  token-gated · no dashboard · no MCP     │     │  webhook seam · demo lab                  │
 └────────────────────┬─────────────────────┘     └────────────────────┬─────────────────────┘
                      │                                                │
                      └──────────────────┬─────────────────────────────┘
                                         ▼
                           ┌─────────────────────────┐
                           │  shared CaseStore       │
                           │  persist → data/cases.json
                           └───────────┬─────────────┘
                                       │
              ┌────────────────────────┼────────────────────────┐
              ▼                        ▼                        ▼
        pavo.mjs                 inference.mjs            telephony.mjs
   demand-conditioned         OpenAI Responses           sandbox | fail-closed
        routing                 + local fallback              live adapter

Deep dive: docs/ARCHITECTURE.md · full diagram: docs/ARCHITECTURE_DIAGRAM.md · pitch architecture slide in assets/architecture.png.

<img src="assets/architecture.png" alt="RxRelay detailed architecture diagram" width="100%" />


🚀 Quickstart

git clone https://github.com/vnmoorthy/rxrelay.git
cd rxrelay
cp .env.example .env
npm test        # 27 tests · node:test · no install step
npm run deck    # rebuild PPTX → deck/output/… ; HTML at deck/pitch.html
npm run dev     # http://localhost:3000

There is nothing to npm install. The sandbox demo has zero runtime dependencies — Node 20+ provides the HTTP server, test runner, --env-file-if-exists, and fetch.

Default mode is TELEPHONY_PROVIDER=demo with ALLOW_LIVE_TELEPHONY=false, so the entire flow runs without dialing or texting a real person.

Pitch deck

Demo in 100 seconds

  1. Open RX-1048 (consent already recorded).
  2. Call pharmacy → sandbox coordination action.
  3. Record blocker → prior authorization needed.
  4. Record clinic step → follow-up submitted.
  5. Confirm readiness → pharmacy outcome + consented sandbox SMS.
  6. Watch the close gate turn green only at 4/4.
  7. Try an uncertain / unsafe turn in the PAVO lab — upgrade the pipeline or safe-stop; never invent completion.

Full script (dashboard sandbox): docs/DEMO.md · judge live call: docs/JUDGE_DEMO.md (+18026768127)


📞 Live inbound voice

voice-server.mjs is a deliberately isolated TeXML gateway. It shares cases with the dashboard through data/cases.json.

npm run dev            # proof board on :3000
npm run live:inbound   # voice :3001 → public tunnel → point claimed number

live:inbound tries Cloudflare quick tunnel first, then falls back to Serveo (ssh -R … serveo.net) when Cloudflare returns 429/1015. Override with VOICE_TUNNEL=serveo or reuse an existing URL via TUNNEL_PUBLIC_URL=https://….

Then call the claimed number and say:

I consent to a pharmacy status follow-up and text updates.

[!IMPORTANT] Inbound voice ≠ outbound messaging. Live SMS/calls stay disabled until ALLOW_LIVE_TELEPHONY=true and LIVE_ALLOWED_RECIPIENTS contains OTP-verified numbers. The live adapter refuses completion without a provider-issued action id.

OTP helpers:

npm run verify -- +1XXXXXXXXXX
npm run confirm -- +1XXXXXXXXXX 123456

Details: docs/A1MOBILE_LIVE_SETUP.md

Optional LiveKit + OpenAI Realtime (post-hackathon)

Tonight's demo stays on TeXML. ChatGPT Realtime needs media streaming (WebRTC/WebSocket), not TeXML <Gather> turn-taking. The a1 PAVO gateway (hack.a1mobile.com/gw/v1) exposes chat models only (sol / terra / luna) — /realtime returns 404 — so Realtime needs a direct OPENAI_API_KEY, plus LiveKit Cloud, plus switching the claimed number from webhook → SIP using creds from GET /api/numbers/me.

Twilio is a different carrier and is not on the a1mobile claimed DID without leaving hackathon rails. Photon Spectrum (@photon-ai/voice-ts) is a real product (messaging + voice over Photon's gRPC plane) but needs a Photon VOICE_TOKEN and does not answer +18026768127 faster than TeXML tonight.

Scaffold (fails closed until keys exist; does not remove TeXML):

npm run voice:realtime   # checks LIVEKIT_* + OPENAI_API_KEY + A1_SIP_*

🔬 PAVO: route the pipeline, not just the model

Grounded in PAVO: Pipeline-Aware Voice Orchestration with Demand-Conditioned Inference Routing.

A better LLM cannot repair a misheard authorization number.

When a turn is uncertain or carries a critical entity, RxRelay upgrades transcription and reasoning together.

Route Triggered by Pipeline
Fast greetings, simple confirmations fast ASR → compact reasoning
Balanced routine status coordination reliable ASR → tool-aware reasoning
Verified noise, low ASR confidence, names/numbers/dates, prior auth, contradiction high-accuracy ASR → structured verifier
Safe stop clinical advice, emergency cues, Rx changes, controlled inventory, identity data no autonomous action → human handoff

Safe stop is checked first. The router is a readable pure function in src/pavo.mjs.

Research: paper · pavo-bench


🛡 Safety contract

RxRelay does not:

  • give medical advice or interpret symptoms
  • prescribe, change, refill, or transfer a prescription
  • determine insurance coverage or eligibility
  • disclose controlled-medication inventory
  • contact anyone without explicit, scope-limited consent

Urgent medical cues are a handoff, not an automation opportunity. Voice consent requires both a consent phrase and a scope term (pharmacy / status / coordinate / text / update).

Live mode is a configuration decision, never a code-path accident.


🔌 MCP tools

POST /mcp exposes JSON-RPC tools that share the same consent + proof gate as the UI:

Tool Purpose
create_rx_case Create a consent-gated coordination case
record_consent Record explicit patient consent
begin_coordination_call Start a non-clinical pharmacy status call
record_external_outcome Record pharmacy_blocker · clinic_submission · pharmacy_ready
issue_counterpart_link Issue a single-use pharmacy/clinic/insurer attestation link
export_proof_receipt Export a signed hash-chained proof receipt
get_case_brief Return status + deterministic resolution proof
list_human_queue List cases held for human review

No tool can bypass the proof gate.


🥊 How it compares

RxRelay Typical voice agent demo Human switchboard
Completion claim Deterministic proof gate Conversational “done” Memory / sticky notes
Uncertain audio Upgrade ASR and reasoning (PAVO) Hope the LLM repairs it Ask the patient to repeat
Clinical / emergency language Safe stop → human Often continues Escalates unevenly
Failed provider call No action evidence recorded Often narrates success Unknown
Outbound contact OTP allowlist + consent Frequently unconstrained Manual
Public blast radius Voice-only process Full app exposed N/A

🏆 Built for the a1mobile Voice AI Hackathon 2026

Criterion Evidence
Idea & creativity Moves voice agents from talking → evidence-backed access coordination
Real-world value Removes patient-as-switchboard work in prescription access
Technical execution Case state machine, PAVO routing, TeXML gateway, counterpart portal, signed receipts, human ops, SSE, MCP (8), proof gate, CI + 27 tests
Voice UX Voice-first consent, confirmation on uncertain critical details, explicit safe stops
Works live Sandbox E2E today; real inbound TeXML path; live outbound fails closed until provider accepts + returns an id

🧠 Related work

Research

Systems by the same author


📁 Repository map

src/pavo.mjs              PAVO-inspired demand-conditioned routing (pure function)
src/inference.mjs         Guarded OpenAI-compatible client + local fallback
src/dialogue.mjs          Phone turn shaping, ASR repairs, TeXML Say helpers
src/voice-lexicon.mjs     Consent / intent paraphrase expansion
src/voice-training/       Mined lexicon + few-shot exemplars for Maya
src/store.mjs             Consent-gated case state machine + proof gate
src/persist.mjs           Shared local JSON store for dashboard + voice
src/telephony.mjs         Sandbox adapter + fail-closed live provider adapter
src/receipt.mjs           Signed hash-chained proof receipts
src/counterpart.mjs       Magic-link attestation tokens
src/bus.mjs               Case event bus for live SSE
server.mjs                HTTP API, webhook seam, MCP endpoint, proof board
voice-server.mjs          Token-protected TeXML inbound gateway
scripts/                  live:inbound · point · verify · confirm
public/                   Proof-board dashboard
site/                     Marketing site → GitHub Pages
assets/                   Social preview + pitch visuals for the README
docs/                     Architecture, live setup, DEMO, JUDGE_DEMO
deck/                     13-slide HTML + PPTX (`npm run deck`)
test/                     node:test suite (27) — routing, consent, proof honesty

🤝 Contributing

See CONTRIBUTING.md and the CODE_OF_CONDUCT.md.

npm run check
npm test

Security reports: SECURITY.md — especially anything that could fabricate proof or skip consent.

License

MIT

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured