Monorepo layout
Two apps, no shared packages. Each app has its own package manager, its own tests, and its own CI. The only thing they share is the CLAUDE.md files at the root and at each app root.
agnt/
├── agnt-backend/ # FastAPI Python service
│ ├── app/
│ │ ├── main.py # FastAPI app, lifespan, middleware
│ │ ├── config.py # 63 env vars, typed via pydantic-settings
│ │ ├── core/ # Business logic
│ │ │ ├── msg_router.py # Main message handler
│ │ │ ├── llm_gateway.py # LLM abstraction (Claude / Ollama)
│ │ │ ├── tool_executor.py # Tool registry + safe dispatch (16 tools)
│ │ │ ├── clawpulse.py # A2A gateway client
│ │ │ ├── a2a_booking.py # Booking envelope orchestration
│ │ │ ├── a2a_enums.py # Envelope + booking state machines
│ │ │ ├── agent_registry.py # Consumer / venue / platform agent DNA
│ │ │ ├── session_store.py # Redis chat history (Fernet encrypted)
│ │ │ ├── credit_manager.py # SELECT FOR UPDATE balance writes
│ │ │ ├── commerce.py # Fees, Stripe idempotency, ledger
│ │ │ ├── scheduler.py # 40+ APScheduler jobs
│ │ │ └── tools/ # venue_search, booking, dupe_search, ...
│ │ ├── routers/ # HTTP routers mounted in main.py
│ │ │ ├── api.py # 60+ REST endpoints
│ │ │ ├── a2a_public.py # /a2a/v1 external agent API
│ │ │ ├── webhook.py # Telegram/WhatsApp/Instagram webhooks
│ │ │ ├── stripe_webhook.py # Payment events
│ │ │ ├── health.py # /health, /metrics, /admin debug
│ │ │ └── ...
│ │ ├── db/
│ │ │ ├── models.py # 47 SQLAlchemy models
│ │ │ └── session.py # async engine (pool=100)
│ │ ├── features/ # Feature sub-packages (scan)
│ │ ├── middleware/
│ │ │ └── agent_auth.py # Bearer token → ApiKey
│ │ ├── mcp/
│ │ │ └── agnt_network_server.py # MCP server at /mcp/sse
│ │ └── cron/ # Nightly jobs (dreaming, graph, faq)
│ ├── alembic/versions/ # Sequential migrations
│ └── tests/ # 84+ pytest tests
│
└── agnt-pwa/ # Next.js 16 PWA
└── src/
├── app/
│ ├── layout.tsx # Root layout, fonts, JSON-LD
│ ├── (public)/ # Marketing + docs routes
│ ├── (authed)/ # Consumer app + venue admin
│ └── api/ # 51 route handlers proxying backend
├── components/ # UI components by domain
│ ├── marketing/ # MarketingNav, Footer, AmbientBg
│ ├── landing/ # LandingHero, LandingProtocol, ...
│ ├── motion/ # Reveal, StaggerGroup, Parallax, CountUp
│ ├── trust/ # FAQ, Testimonials, LogoBar
│ ├── docs/ # DocsShell, Sidebar, Pager, Callout
│ └── ...
└── lib/
├── api.ts # backendFetch, BackendError
├── motion.ts # Ease, duration, stagger constants
└── ...Request lifecycle
A single WhatsApp booking touches 8 layers on the way through. Below is what happens when a user types "book sunset dinner for 2" into their WhatsApp chat with the AGNT bot.
1. Webhook lands on the backend
WhatsApp Cloud API (via 360dialog) POSTs to agnt-backend/app/routers/webhook.py. The handler verifies the signature against META_APP_SECRET and hands the message to the message router.
2. Message router dispatches
app/core/msg_router.py is the single entrypoint for text and image messages from every channel. It looks up the user, loads the session history from Redis, classifies the intent, and routes to the right handler.
3. LLM gateway builds the prompt
app/core/llm_gateway.py abstracts the LLM call. It loads the soul prompt from soul_loader.py (base system prompt + user preferences + memory chunks + weather + patterns), attaches the 16 tool schemas from tool_executor.py, and calls Anthropic's Messages API with a token budget.
4. Tool executor runs
If the model returns a tool_use, the executor strips reserved keys, clamps string lengths, and calls the registered function with a 5 second timeout. For a booking, that's tools/booking.create_booking.
5. ClawPulse sends the A2A envelope
The booking tool delegates to core/a2a_booking.send_booking_envelope, which signs the payload with HMAC-SHA256 and POSTs to ClawPulseClient.send_message(to_agent_id=venue-...). The global circuit breaker is checked first, then the per-venue one, then the call goes to the ClawPulse gateway.
6. Venue agent drains the inbox
The venue's inbox loop (core/venue_inbox_loop.py) polls /api/agents/{id}/queue/drainevery 30–60 seconds. It pops the booking request, checks the diary, and responds with either accepted or rejected.
7. Consumer listener picks up the response
core/a2a_response_listener.pyis spawned per booking and polls the consumer's inbox for up to 10 minutes. When the response arrives, it validates the state transition against ENVELOPE_TRANSITIONS and persists the new status.
8. User is notified and billed
On accepted, the listener queues a WhatsApp outbound via the send queue (channel_sender.py), writes a calendar entry, and (for paying tiers) emits a metered ApiKeyUsageEvent that the nightly _job_charge_unbilled_usage batches to Stripe.
Data stores
Three stores, each with a distinct job:
- PostgreSQL 16 + pgvector— source of truth for users, venues, bookings, envelopes, fees, CRM, knowledge chunks. 45 models in
app/db/models.py, async engine withpool_size=100, max_overflow=50. - Redis 7— session chat history (Fernet encrypted, platform-scoped), rate-limit counters, distributed locks for the scheduler, the outbound send queue, and idempotency guards.
- ClawPulse gateway— not a data store exactly, but functionally the message broker for agent-to-agent traffic. Inboxes live inside ClawPulse; the backend only holds envelope state locally.
Deployment
Backend runs on Railway at api.agntdot.com. Frontend runs on Vercel at app.agntdot.com. Postgres and Redis are managed services. The scheduler runs inside the same backend process but is leader-elected via a Redis lock so only one worker acts on each job.