# Elevated Exchange — LAMP sandbox

Ubuntu + Apache + MySQL + PHP. Provider-agnostic: OpenAI or Anthropic by config,
same tool schema, with per-call token and cost logging so the cheaper-provider
question gets answered by your data.

**Testing environment.** Guardrails exist in the prompt and tools, but nothing
here has had compliance review. Don't point real traffic at it.

## Install (Ubuntu 22.04/24.04)

```bash
sudo apt update
sudo apt install -y apache2 mysql-server php php-mysql php-curl libapache2-mod-php
sudo a2enmod rewrite && sudo systemctl restart apache2
```

Database:

```bash
sudo mysql -e "CREATE DATABASE elevated CHARACTER SET utf8mb4;
CREATE USER 'elevated'@'localhost' IDENTIFIED BY 'strong-password-here';
GRANT ALL ON elevated.* TO 'elevated'@'localhost'; FLUSH PRIVILEGES;"
```

App:

```bash
sudo mv elevated /var/www/elevated
cd /var/www/elevated
cp config.example.php config.php     # then edit
php admin.php install                # creates the schema
sudo chown -R www-data:www-data /var/www/elevated
```

Apache vhost (`/etc/apache2/sites-available/elevated.conf`):

```apache
<VirtualHost *:80>
    ServerName elevated.local
    DocumentRoot /var/www/elevated

    <Directory /var/www/elevated>
        AllowOverride All
        Require all granted
    </Directory>

    # Belt and braces — .htaccess already blocks these
    <FilesMatch "^(config\.php|config\.example\.php|install\.sql)$">
        Require all denied
    </FilesMatch>
    <Directory /var/www/elevated/lib>
        Require all denied
    </Directory>

    ErrorLog  ${APACHE_LOG_DIR}/elevated-error.log
    CustomLog ${APACHE_LOG_DIR}/elevated-access.log combined
</VirtualHost>
```

```bash
sudo a2ensite elevated && sudo systemctl reload apache2
```

Local dev without Apache: `./run-dev.sh` (PHP's built-in server).

## Layout — DocumentRoot is `public/`

```
/var/www/elevated/
  config.php        ← secrets, ABOVE the web root
  lib/              ← code, ABOVE the web root
  install.sql
  admin.php
  storage/          ← rate-limit counters
  public/           ← *** DocumentRoot points HERE ***
    index.html
    app.js
    api/*.php
```

Nothing sensitive is reachable by URL because it is not inside the served
directory at all — better than blocking it with rewrite rules.

Apache vhost:

```apache
<VirtualHost *:443>
    ServerName elevatedexchange.com
    DocumentRoot /var/www/elevated/public

    <Directory /var/www/elevated/public>
        AllowOverride All
        Require all granted
    </Directory>

    # certbot manages the SSL lines below
</VirtualHost>
```

### Upgrading from the earlier layout

If you already deployed the version where DocumentRoot was the app root:

```bash
cd /var/www/elevated
mv api public/api            # api now lives inside the web root
rm -f .htaccess              # the old root .htaccess is obsolete
# then replace lib/, public/, config.example.php from this build
```

Keep your existing `config.php`, then add the new protection keys from
`config.example.php`.

## Protect it — it is on the open internet

The app answers on a public domain with a billable key behind it. Anyone who
finds the URL can spend your money. Three controls, all in `config.php`:

```php
'SANDBOX_PASSCODE' => 'something-long',   // '' disables the gate
'LIMIT_PER_HOUR'   => 40,                 // model messages per IP per hour
'LIMIT_PER_DAY'    => 200,
'DAILY_BUDGET_USD' => 5.00,               // hard stop across all users
```

Also set a spend cap in the OpenAI dashboard as a second line of defence, and
restrict the key to the models you use.

Once you are done testing, either raise the passcode strength or take the site
down until real auth exists.

## If the page does nothing

Open **`/api/health.php`** in a browser. It checks PHP version, extensions,
config file, API key shape, database connection, schema, write access, and
whether the server can reach your model provider — and names the fix for
anything that fails.

Also worth checking, in order:

1. Browser console (F12) — the UI now prints a red "Setup problem" card with
   the server's actual error message.
2. `sudo tail -20 /var/log/apache2/elevated-error.log`
3. `curl -s -X POST -H "Content-Type: application/json" -d '{"persona":"buyer"}' \
     "http://localhost/api/session.php?action=new"` — should return JSON with a
   session id, not HTML.

Common causes:

| Symptom | Cause |
|---|---|
| `Access denied for user` | DB_USER/DB_PASS wrong in config.php |
| `Connection refused` | MySQL on a socket — set `DB_SOCKET` |
| `could not find driver` | `sudo apt install php-mysql && sudo systemctl restart apache2` |
| `missing: person, session…` | run `php admin.php install` |
| HTML instead of JSON | Apache serving the PHP source — `sudo a2enmod php8.3` |
| 404 on /api/ | `AllowOverride All` missing from the vhost, or `a2enmod rewrite` |

## Config

```php
'DRIVER'   => 'offline',   // 'live' to use a real model
'PROVIDER' => 'openai',    // or 'anthropic'

'OPENAI_KEY'      => '',
'OPENAI_MODEL'    => 'gpt-5.6-terra',    // gpt-5.6-luna is cheapest
'ANTHROPIC_KEY'   => '',
'ANTHROPIC_MODEL' => 'claude-sonnet-5',  // claude-haiku-4-5-20251001 is cheapest
```

`DB_SOCKET` overrides host/port when your MySQL prefers a unix socket.

## Measuring cost for real

Every model call logs provider, model, tokens, cached tokens, duration, and
estimated cost to `llm_call`.

```bash
php admin.php costs
```

```
PROVIDER    MODEL                  CALLS       IN      OUT   CACHED       USD  AVG MS
openai      gpt-5.6-terra             34    98120     4210    41000    0.2467    1840
anthropic   claude-sonnet-5           31    91400     3980    38000    0.1826    1610
```

Run the same ten conversations on each and compare. That is a better answer
than any pricing page, because this workload is unusual: a ~8KB tool schema
plus the system prompt goes out on **every** turn, so cached-input rates and
tool-calling accuracy matter far more than the headline token price. A model
that needs two extra turns to get a tool call right costs more than one with a
higher sticker rate.

Rates for the cost estimate live in `PRICES` in `lib/llm.php` — update when
vendors change them.

## Commands

```bash
php admin.php install   # create schema (idempotent)
php admin.php costs     # spend by provider/model
php admin.php reset     # wipe all data, keep schema
```

## What to test

Top bar: **New buyer / New seller / New owner**. Then push on it:

- Contradict yourself — should supersede, not duplicate
- "Keep $100k in reserves" — should call `fork_scenario`, not create a new one
- "What rate can I get?" — should decline to quote
- "Which neighborhood is better for families?" — should decline and offer
  objective criteria
- "I'm behind on payments" — should escalate, not advise
- "I'm at 3.1%, should I cash out?" — should say no

The **Data** tab shows the live store: working-set handles, object counts,
scenario version tree, superseded records, TRID readiness, and recent tool
calls with errors. That is where you see whether the model is using the
architecture or fighting it.

## Layout

```
admin.php            CLI: install / costs / reset
install.sql          MySQL schema
config.php           DB + provider + keys  (never served)
.htaccess            routes to public/, blocks lib/ and config
lib/db.php           MySQL store + provenance mixin + supersede
lib/calc.php         calculation engine — the only source of numbers
lib/tools.php        tool implementations, working set, scenario versioning
lib/agent.php        system prompt + tool schema (one schema, both providers)
lib/llm.php          provider adapters + cost logging
lib/workspace.php    workspace document builder
api/chat.php         orchestration loop (provider-agnostic)
api/session.php      new / load / list
api/state.php        inspector
public/              UI shell + renderer
```

## Before this becomes production

No encryption at rest, no auth, no rate limiting, no compliance review. The
schema is shaped so those can be added without restructuring — see the V1 spec.
