# How to add an AI agent to your Django or FastAPI app

To add an AI agent to your Django or FastAPI app, create the agent from a chat with the Vatio MCP server, then build it into the code with `npx @vatio-ai/skills` and `/vatio-init` ([steps below](#add-the-agent-in-two-steps)). Your coding agent reads your app. The agent shows up as a chat widget in your base template. To act for the signed-in user, your app signs a short-lived token with PyJWT, Vatio verifies it, and the agent's private tools call your own endpoints, which verify the same token and answer only for that user. Your team supervises the conversations in the inbox, and nothing reaches your users until you publish.

## Add the agent in two steps

**Step 1, from any chat.** Add the remote MCP server `https://vatio.ai/mcp` as a connector in Claude, ChatGPT or Cursor, sign in with a Vatio account and ask for the agent. It is created in a preview you can try; nothing reaches your users until you publish ([MCP](https://vatio.ai/docs/mcp)).

**Step 2, build it into the Django or FastAPI code.** In the project, run:

```text
npx @vatio-ai/skills
/vatio-init
```

Run the first line in a terminal at the root of your project, and the second in Claude Code (in Cursor or Codex, ask it to use the vatio-init skill). `/vatio-init` looks at what already exists, then writes the agent and pushes a preview. The first time it pushes, your browser opens to sign in with email, Google or GitHub. Then ask your coding agent to put the widget script tag in your base template. You can say what you want after `/vatio-init`:

```text
/vatio-init a support agent for our Django app: answer from our docs and look up the signed-in user's invoices
```

After launch, `/vatio-improve` is the daily routine ([After launch](#after-launch)).

## What vatio-init finds in a Django or FastAPI project

vatio-init reads your app before it writes anything, tells you what it found and what it recommends, and then asks only what the code can't answer: what the agent may do, what it must never say or promise, and when a person takes over. In a Django or FastAPI project, that means:

- **What the agent could call:** your `urls.py` and views (Django REST Framework included), or your FastAPI routers.
- **How your users sign in:** `django.contrib.auth` or django-allauth, a FastAPI security dependency, or Supabase. Whichever it is, your backend signs the token ([Authentication](https://vatio.ai/docs/authentication/)); on Supabase Auth, an Edge Function can ([Supabase apps](https://vatio.ai/docs/guides/ai-agent-for-supabase-apps)).
- **What it could answer from:** your README, a `docs/` folder, help and policy pages. What the agent needs from them becomes knowledge entries.

## Put the agent in your app

Ask your coding agent to add this code; it is shown here so you can review it.

The widget is one script tag in your base template, before `</body>`. Django templates and the Jinja2 templates FastAPI renders read it the same way:

```html
<script async src="https://cdn.vatio.ai/v1/widget.js" data-workspace="ledgerly"
        data-token="{{ vatio_publishable }}"
        {% if vatio_token %}data-visitor-token="{{ vatio_token }}"{% endif %}></script>
```

`data-token` takes a publishable `vatpub_` key, made for page source: it selects one workspace and environment, and works only from the origins listed on it, plus Vatio's own pages and the app builders. Create one for production with `vatio keys create publishable --env live --label web --origin https://app.ledgerly.example`. Origins are exact `scheme://host[:port]` values, and a refused one is named in the browser console. They live on the token, never in `vatio.yml`: change them with `vatio keys origins PREFIX URL...` or in **Settings → API keys** in the inbox. Never put a `vat_` developer token in a page. If your FastAPI app serves a separate frontend, return the user's token from an endpoint and pass it to `window.VatioWidget.identify(token)` instead.

## Let it act for signed-in users

`vatio auth --new-key` writes the key pair. `identity.pub` stays in the workspace, named in `vatio.yml`; your app gets its contents as `VATIO_IDENTITY_PUBLIC_KEY`, and `identity.pem` as `VATIO_IDENTITY_PRIVATE_KEY`, never in `vatio secrets` or git. Vatio holds only the public half, so it can check a token but never mint one. Your coding agent can add one module that signs and verifies:

```python
# vatio_identity.py
import os
import time
import jwt  # pip install "pyjwt[crypto]"

WORKSPACE = "ledgerly"  # your workspace slug: the token's aud

def widget_context(user):  # user is None when nobody is signed in
    token = jwt.encode(
        {"sub": str(user.id), "aud": WORKSPACE, "exp": int(time.time()) + 3600, "email": user.email},
        os.environ["VATIO_IDENTITY_PRIVATE_KEY"], algorithm="RS256",
    ) if user else None
    return {"vatio_publishable": os.environ["VATIO_PUBLISHABLE_TOKEN"], "vatio_token": token}

def vatio_subject(authorization):  # the user id from a forwarded token, or None
    try:
        claims = jwt.decode(
            authorization.removeprefix("Bearer "), os.environ["VATIO_IDENTITY_PUBLIC_KEY"],
            algorithms=["RS256"],  # pinned, never read from the token's header
            audience=WORKSPACE, options={"require": ["sub", "aud", "exp"]})
    except jwt.InvalidTokenError:  # bad signature, wrong aud, expired, missing
        return None
    return claims["sub"]
```

In Django, a context processor puts the widget's values in every template, and the tool's endpoint is a plain view:

```python
from django.http import JsonResponse
from .models import Invoice
from .vatio_identity import vatio_subject, widget_context

def vatio(request):  # add to TEMPLATES' "context_processors"
    return widget_context(request.user if request.user.is_authenticated else None)

def my_invoices(request):  # path("api/me/invoices", views.my_invoices)
    user_id = vatio_subject(request.headers.get("Authorization", ""))
    if user_id is None:
        return JsonResponse({"error": "unauthorized"}, status=401)
    invoices = Invoice.objects.filter(user_id=user_id).values("number", "status", "total")[:10]
    return JsonResponse({"invoices": list(invoices)})
```

A view a tool sends a `POST` to needs `@csrf_exempt`: the verified token, not a cookie, is what authenticates it. In FastAPI, a route that renders a page passes the same values to its template, `templates.TemplateResponse(request=request, name="home.html", context=widget_context(user))`, with `user` from your own sign-in dependency, or `None`. The tool's endpoint reads the header:

```python
from fastapi import Header, HTTPException
from .vatio_identity import vatio_subject

@app.get("/api/me/invoices")
def my_invoices(authorization: str = Header("")):
    user_id = vatio_subject(authorization)
    if user_id is None:
        raise HTTPException(status_code=401, detail="unauthorized")
    return {"invoices": invoices_for(user_id)}  # your query, scoped to user_id
```

**The user comes from the token, never from a parameter.** A private tool cannot declare a user id (`user_id`, `customer_id`, `account_id` and similar) as a parameter, and the deploy fails if it does. Scope every query by the id `vatio_subject` returns ([Private tools](https://vatio.ai/docs/authentication/tools)).

## What your coding agent writes

These files live in your repository, so you can review them like the rest of your code. Below are an excerpt of `vatio.yml` and `tools/my_invoices.yml`, a private tool with no user id in it: your endpoint reads the user from the token. `LEDGERLY_API`, your app's address, is a secret your coding agent stores in Vatio with `vatio secrets set`, never in the repository. [Tools](https://vatio.ai/docs/tools) covers tools that act, errors and statuses.

```yaml
workspace: ledgerly
auth:
  public_key: identity.pub
agent:
  instructions: |
    You help signed-in Ledgerly users with their invoices and how-to questions.
    Only state what my_invoices returned. Never promise a refund or a credit.
  tools: [my_invoices, knowledge_lookup]
  knowledge: [docs]
```

```yaml
description: The signed-in user's recent invoices, with status and total.
when_to_use: The user asks about their own invoices, payments or charges.
access: private
request:
  method: GET
  base_url: "$env.LEDGERLY_API"
  path: /api/me/invoices
  headers:
    Authorization: "Bearer $auth.token"
respond:
  data:
    invoices: "$.invoices"
```

## Try it, then publish

vatio-init's push ends with a link to the inbox, with **Try the agent** open, where you try the preview on WhatsApp, Instagram or a web chat. The web chat is a signed-out visitor, so it only reaches public tools. To try what a signed-in user gets, create a preview token for the address `runserver` or `uvicorn` prints (`vatio keys create publishable --env preview --label dev --origin http://127.0.0.1:8000`), use it as `VATIO_PUBLISHABLE_TOKEN` there and sign in as a test user. Tools run on Vatio's servers and can't reach `localhost`, so point preview at a tunnel with `vatio secrets set LEDGERLY_API https://YOUR-TUNNEL --env preview`; live keeps calling production. Ask about your own invoices and check them against your app, then ask for someone else's by name or email: it should find nothing. Each reply in the inbox shows the tool calls behind it, with what your endpoint answered. When the agent does what you want, ask your coding agent to publish.

## After launch

Your team reads real conversations in the inbox. On a wrong reply, **Report this reply** asks what should have happened, and that note becomes a test. Once a day, run `/vatio-improve` in your project: your coding agent fixes the cause where it lives (the instructions, a knowledge entry, a tool or your endpoint) and pushes it to preview, and you review the results and approve the update ([Improve](https://vatio.ai/docs/improve)). The same agent answers on WhatsApp and Instagram ([Pricing](https://vatio.ai/pricing)). There a private tool sends a **Sign in** button to your login page, which redirects back with the same token ([On WhatsApp and Instagram](https://vatio.ai/docs/authentication/sessions#on-whatsapp)).

## Frequently asked questions

### How do I add an AI agent to my Django or FastAPI app?

Step 1: add the MCP server https://vatio.ai/mcp as a connector in Claude, ChatGPT or Cursor, sign in and ask for the agent; it is created in a preview. Step 2: in your project run npx @vatio-ai/skills, then /vatio-init in Claude Code (in Cursor or Codex, ask it to use that skill). The widget is one script tag in your base template.

### How does the agent know which user is signed in to my Python app?

Your app signs a short-lived JWT for the signed-in user with PyJWT, using RS256 and the private key from vatio auth --new-key, with sub set to the user's id, aud to your workspace slug and an exp. The template renders it on the widget as data-visitor-token. Vatio verifies it with your public key, the only half it holds, so it can check a token but never create one.

### How should a Django or FastAPI endpoint verify a token from a Vatio tool?

Decode it with jwt.decode, passing the public key, algorithms limited to RS256 so the algorithm is pinned, audience set to your workspace slug, and sub, aud and exp required. PyJWT rejects an expired token. Answer 401 when anything fails, and take the user from the token's sub, never from the request body or query.

## Related guides

- [Build an AI agent for your app](https://vatio.ai/docs/guides/build-an-ai-agent): The two-step path with your coding agent, from the first chat to publishing.
- [An AI chat for your web app or site](https://vatio.ai/docs/guides/ai-chat-widget-for-your-website): One script tag puts the agent in your web app or site, themed to your brand and able to recognize signed-in users.
- [Support for signed-in users](https://vatio.ai/docs/guides/ai-agent-for-customer-support): Answer from your docs, check the user's own account, troubleshoot and open a ticket with what was tried.
