Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

56 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Capia

Say what you want and what you'll pay. Approve once with your passkey. An agent watches the price and the stock, and buys the moment your conditions are met — unattended, inside a cap enforced by the card network rather than by our code.

Live: capia-ai.netlify.app · API capia.onrender.com · MCP https://capia.onrender.com/mcp


What it actually does

You describe a purchase in your own words — "the titanium wallet on ridge.com if it drops under $50, at most $60 all-in". Capia compiles that into a typed rule, shows you real products with live prices, and asks you to approve a spending limit with your passkey. That approval mints a Prava mandate: a standing authorization Visa itself enforces, scoped to one store and capped at the number you approved.

Then you close the laptop. A watcher polls the merchant, and when your condition fires it quotes the real total, checks it against the cap, charges the mandate, and attempts the purchase. Every attempt — bought, refused, missed, failed — lands in a ledger with the reason and the merchant's own error code.

The design claim is about what the agent cannot do. It can create watches, pause them, cancel them and lower a limit. It cannot approve a mandate, raise a limit, change your delivery address, or spend a cent. Those are not permission checks that could be bypassed — the tools simply do not exist, and the cap is enforced by Visa rather than by an if statement in this repository.

Status, honestly

Step State
Natural language → typed rule works
Store discovery ("who sells a mattress?") works
Live catalogue, real prices, real variants works
Binding quote incl. tax and shipping works
Mandate approved by passkey, network-enforced works
Charge against the mandate, Visa credential minted works
Drawdown and settlement reporting works
Merchant checkout completion blocked — see below

The last step is refused by the merchant, not by us. Shopify's UCP complete_checkout returns -32000 AuthenticationRequired because Capia is not a Shopify-signed agent. That refusal is captured, reported to Prava as DECLINED, and shown in the ledger with the code.

This is not a Ridge quirk. We probed 91 well-known brands: 50 serve a live UCP manifest, and not one advertises a payment capability. The wall is agent identity, and it is the whole ecosystem's, not ours. Prava's Browser Harness is the intended way through it; driving it end to end gets as far as the merchant's checkout and is then stopped by Shopify's bot captcha.

So: everything up to the merchant's door is real, including money moving against a real mandate. The door is shut to unaccredited agents today.

Four ways in, one core

Every surface passes the user's words through verbatim and lets Capia compile them. None of them can approve spending.

  • Web chat — describe it, pick from real products, approve with a passkey
  • Voice — speak instead of typing; the transcript lands in the composer for you to read before it is sent, because speech recognition is allowed to be wrong but not silently wrong
  • MCP — any agent (Claude Code, Codex, Cursor, VS Code, Windsurf, Zed) over streamable HTTP with a scoped, revocable token
  • WhatsApp — inbound message → watch, entirely inside the 24-hour service window so no message templates are needed. Backend complete and tested; not currently surfaced in the UI.

Integrations

Prava — payments. Mandates are created per watch, scoped to the actual store, and capped at exactly the amount approved. Charging mints a single-use Visa network credential; Capia never holds a reusable card number and stays out of PCI scope. Charges are reported back so the lifecycle closes — a correctly reported decline releases its drawdown.

UCP (Shopify's Universal Commerce Protocol) — discovery, catalogue and quoting across 23 verified stores. "Verified" means two things: the domain serves a live /.well-known/ucp manifest and a real catalogue search returns priced, in-stock variants.

Senso — store discovery. A knowledge base of store profiles built from observed catalogue data plus each merchant's own description of itself, so "I need a mattress" finds Casper even though the word "mattress" appears nowhere in Casper's product titles. Grounded or silent: an answer without citations is discarded, and "none of our stores sell that" is a first-class answer.

OpenAI — the intent compiler (structured outputs) and speech-to-text.

Run it

cp .env.example .env      # works unedited; add keys when you need them
docker compose up

Postgres, migrations, API on :8000, web on :3000. Verified from a destroyed volume — migrations run at container start, so a clean clone comes up with a correct schema in one command.

Web http://localhost:3000
API http://localhost:8000
API docs http://localhost:8000/docs (non-production only)
Health http://localhost:8000/health

Use localhost, never 127.0.0.1 — WebAuthn binds to an RP ID and an RP ID can never be a bare IP.

Backend directly

Faster than rebuilding an image on every edit. Postgres still comes from compose.

docker compose up -d postgres
cd backend
python3 -m venv .venv && ./.venv/bin/pip install -e ".[dev]"
cp .env.example .env

./.venv/bin/alembic upgrade head
./.venv/bin/uvicorn app.main:app --reload --port 8000
./.venv/bin/pytest                       # 286 passing
./.venv/bin/ruff check app tests alembic

Tests need their own database — createdb capia_test, or:

docker exec capia-postgres-1 psql -U capia -d postgres \
  -c "CREATE DATABASE capia_test OWNER capia;"

Schema changes:

./.venv/bin/alembic revision --autogenerate -m "what changed"
./.venv/bin/alembic upgrade head

Frontend directly

cd frontend && pnpm install && pnpm dev

Seed the store knowledge base

Needed once for discovery, and again whenever the store list changes. Re-running refreshes in place rather than duplicating.

cd backend && ./.venv/bin/python -m app.knowledge.seed

Connecting an agent

Sign in, open Assistants, pick your client, and copy the one command it gives you. Or by hand:

claude mcp add --transport http capia https://capia.onrender.com/mcp \
  --header "Authorization: Bearer cap_..."

Twelve tools: propose_watch, confirm_watch, list_stores, find_stores, browse_store, check_approval, list_watches, get_watch, pause_watch, resume_watch, cancel_watch, lower_limit.

There is deliberately no tool to raise a limit and none to approve a mandate.

Tokens are minted from your signed-in session, shown once, stored only as a SHA-256 digest, and revocable from the same page.

Layout

backend/
  app/
    config.py        all settings; nothing else reads os.environ
    core/
      database.py    engine, Base, TimestampMixin, get_session
      security.py    bcrypt, access/refresh JWT, opaque token digests
      money.py       integer minor units; floats rejected at the boundary
      registry.py    Registry[T] — providers and fulfillers by name
      signals.py     SignalProvider, Reading
      conditions.py  edge-triggered conditions
      fulfillment.py Fulfiller, AuthorizationRef, Quote, Order
      policy.py      PolicyGate — pure, deterministic spend rules
      engine.py      one tick: observe → decide → quote → charge → record
      watcher.py     the loop (FOR UPDATE SKIP LOCKED, heat tiers, jitter)
    intent/          natural language → typed watch rule
    prava/           Prava REST client
    providers/       UCP catalogue + signal provider
    fulfillers/      UCP fulfiller
    knowledge/       Senso client and the store-profile seeder
    channels/        WhatsApp Cloud API client
    mcp_server/      MCP server, PAT auth middleware
    services/        business logic — routers stay thin
    api/             routers
  alembic/           migrations
frontend/            Next.js 16, React 19, Tailwind 4

Authentication

Three ways into one account, because how you prove identity is deliberately separate from who you are:

  • Password — bcrypt, access + refresh JWT with rotation, email verification, reset
  • Google — authorization code + PKCE, exchanged server-side
  • Passkey — WebAuthn, enrolled from an existing session as an additional method

Adding Apple or GitHub later is a row in auth_identities, not a schema change.

Worth knowing:

  • Refresh tokens rotate. Presenting an already-rotated token is treated as theft: every session for that user is revoked, and the revocation is committed before the error is raised, because the request session rolls back on exception.
  • Only digests are stored for refresh, verification, reset and agent tokens.
  • Login is constant-ish time and returns an identical error for an unknown email and a wrong password. /auth/forgot-password responds identically either way.
  • Token types are not interchangeable. A refresh token presented as a bearer token is rejected.
  • Federated email linking only happens when the provider verified the email.
  • Password reset revokes every existing session.

Conventions

Type hints everywhere. ruff clean, line length 100. Async throughout — no blocking IO in request paths. Config only via app.config.Settings. Money as integer minor units with Decimal confined to adapter boundaries; never float.

PRAVA_LIVE defaults false, is required to construct anything that can spend real money, and is reported by /health so a mistake is visible in one request.

Third-party surfaces fail open where they inform (discovery degrades to the full store list) and closed where they authorize (an unsigned webhook is refused outright).

Disclosure

Built before the hackathon window opened (Aug 1, 07:30 IST):

  • Project scaffolding, dependency setup, lint/test configuration, Dockerfiles and docker-compose.yml
  • The full identity layer above — password, Google OAuth, passkeys, refresh rotation, verification, reset — with 32 tests
  • Postgres schema and the initial Alembic migration
  • Landing and auth pages, from a design generated before the window
  • Throwaway API reconnaissance scripts (not in this repo, not imported by it) used to learn Prava's REST and MCP surfaces before writing the client

Built during the window: the entire watch engine — signal providers, conditions, the policy gate, the Prava client and fulfiller, the watcher loop, the intent compiler — the MCP server, the Senso discovery layer, voice input, the WhatsApp channel, and all product UI beyond sign-in.

About

Say what you want and what you'll pay. Approve once with a passkey. An agent buys it unattended inside a cap Visa enforces — over chat, voice, or MCP. Any agent can use it; no agent can spend.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages