Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 54 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,61 @@
# Copy this file to .env and fill in your values
# NEVER commit your .env file to git
# ───────────────────────────────────────────────────────────────────────────
# Sim2Sim — environment variables
# Copy this file to .env and fill in your values. NEVER commit .env to git.
# ───────────────────────────────────────────────────────────────────────────

# Get your key at: https://console.anthropic.com
# ── Core ───────────────────────────────────────────────────────────────────
# Anthropic API key for AI explanations. Get one at https://console.anthropic.com
ANTHROPIC_API_KEY=sk-ant-your-key-here

# Rate limiting (requests per minute per IP)
RATE_LIMIT_PER_MINUTE=20

# Set to "production" on Replit
# "development" relaxes CORS and bypasses Pro gating with the "dev" license key.
# Set to "production" on Replit.
ENVIRONMENT=development

# Required when ENVIRONMENT=production — comma-separated list of allowed
# origins (e.g. https://sim2sim.app,https://www.sim2sim.app)
ALLOWED_ORIGIN=

# Public base URL of your deployment — used in license-key emails.
APP_BASE_URL=https://sim2sim.app


# ── Licensing ──────────────────────────────────────────────────────────────
# HMAC secret used to sign license keys. Generate a fresh one for production:
# python -c "import secrets; print(secrets.token_urlsafe(48))"
# Required in production; defaults to a dev-only value in development.
SIM2SIM_LICENSE_SECRET=

# Path to the SQLite database holding licenses. Defaults to ./sim2sim.db.
# On Replit, the home directory persists, so the default is fine.
SIM2SIM_DB=sim2sim.db


# ── Stripe ─────────────────────────────────────────────────────────────────
# Stripe webhook signing secret — copy from the Stripe Dashboard:
# Developers → Webhooks → click your endpoint → Signing secret
STRIPE_WEBHOOK_SECRET=whsec_

# Price amounts in cents — used to map checkout sessions to a tier when
# you use Stripe Payment Links (which don't expose price IDs in the event).
# Set these to whatever you charge.
STRIPE_PRICE_PRO_CENTS=4900
STRIPE_PRICE_TEAM_CENTS=24900

# Optional: hosted Payment Link URLs. After creating Payment Links in the
# Stripe Dashboard, paste them here. The pricing page reads these from
# window.SIM2SIM_CONFIG (injected at deploy time) — or you can hard-code
# them in static/pricing.html for a one-time launch.
STRIPE_PRO_PAYMENT_LINK=https://buy.stripe.com/__paste_pro_link__
STRIPE_TEAM_PAYMENT_LINK=https://buy.stripe.com/__paste_team_link__


# ── Email (Resend) ─────────────────────────────────────────────────────────
# Resend API key — get one at https://resend.com (free tier: 100/day)
RESEND_API_KEY=re_

# Default From address. Use "onboarding@resend.dev" while testing; switch
# to a verified sending domain ("hello@sim2sim.app") before launch.
RESEND_FROM=Sim2Sim <onboarding@resend.dev>
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install ruff

- name: Lint (ruff)
run: ruff check src tests main.py
continue-on-error: true # don't fail the build on style; just surface findings

- name: Run tests
env:
ANTHROPIC_API_KEY: "" # explainer tests don't need a real key
run: pytest -q
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ venv/
dist/
build/
.DS_Store

# Billing DB (created at runtime — never commit)
sim2sim.db
sim2sim.db-journal
sim2sim.db-wal
sim2sim.db-shm
109 changes: 109 additions & 0 deletions LAUNCH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Sim2Sim — Launch Checklist

A 30-minute path from "freshly merged PR" to "taking real money".

## 1. Buy your domain (5 min)

Recommended: **sim2sim.app** ($14–20/yr) — short, on-brand, no aftermarket fee.
(`sim2sim.com` is parked at $5,000 — skip it for now.)

Other strong options:
- `sim2sim.tools` — fits the product perfectly
- `getsim2sim.com` or `usesim2sim.com` — keeps you in `.com` for ~$12/yr
- `sim2sim.ai` — premium ($70–100/yr) but great if you want the AI angle

Buy at: [Namecheap](https://namecheap.com), [Porkbun](https://porkbun.com), or [Cloudflare Registrar](https://cloudflare.com/products/registrar/) (no markup).

## 2. Connect domain to Replit (5 min)

1. In Replit: **Deployments → Settings → Custom Domains → Link domain**.
2. Replit shows you a CNAME / A record to add at your registrar.
3. Paste it into your registrar's DNS settings. Wait 5–10 min for propagation.

## 3. Create Stripe Payment Links (5 min)

In your Stripe dashboard:

1. **Product → Add product** — "Sim2Sim Pro", one-time, $49.00.
2. **Product → Add product** — "Sim2Sim Team", one-time, $249.00.
3. Click each product → **Pricing → Create payment link**.
4. Under **After payment**, set **Show confirmation page** to **Redirect** → `https://yourdomain.app/account`.
5. (Optional but recommended) Under **Advanced options → Metadata**, add `tier=pro` (or `team`). Sim2Sim falls back to the amount, but metadata is more robust.
6. Copy both Payment Link URLs.

## 4. Wire the webhook (3 min)

1. Stripe Dashboard → **Developers → Webhooks → Add endpoint**.
2. URL: `https://yourdomain.app/api/billing/webhook`
3. Event: select **`checkout.session.completed`**.
4. Click **Add endpoint**, then reveal **Signing secret** (`whsec_...`).

## 5. Sign up for Resend (2 min)

1. Create an account at [resend.com](https://resend.com).
2. (Optional for launch) Verify a sending domain — until then, emails go from `onboarding@resend.dev`, which works but looks less polished.
3. Copy your API key (`re_...`).

## 6. Set environment variables in Replit (5 min)

In **Replit → Secrets**, add:

| Variable | Value |
|---|---|
| `ENVIRONMENT` | `production` |
| `ALLOWED_ORIGIN` | `https://yourdomain.app` (the one from step 2) |
| `APP_BASE_URL` | `https://yourdomain.app` |
| `SIM2SIM_LICENSE_SECRET` | run `python -c "import secrets;print(secrets.token_urlsafe(48))"` and paste the output |
| `STRIPE_WEBHOOK_SECRET` | from step 4 |
| `STRIPE_PRICE_PRO_CENTS` | `4900` |
| `STRIPE_PRICE_TEAM_CENTS` | `24900` |
| `RESEND_API_KEY` | from step 5 |
| `RESEND_FROM` | `Sim2Sim <hello@yourdomain.app>` (or leave default during testing) |
| `ANTHROPIC_API_KEY` | your existing key |

## 7. Paste your Payment Link URLs (2 min)

Open `static/pricing.html` and replace the two `__configure_me__` placeholders (search for `STRIPE_PRO_URL` / `STRIPE_TEAM_URL`), or — cleaner — add this script block right above `</head>` in `pricing.html`:

```html
<script>
window.SIM2SIM_CONFIG = {
stripeProUrl: "https://buy.stripe.com/your-pro-link",
stripeTeamUrl: "https://buy.stripe.com/your-team-link",
};
</script>
```

## 8. Deploy

In Replit, click **Deploy**. The web server is `uvicorn main:app --host 0.0.0.0 --port $PORT`. The SQLite DB persists in the home directory across deploys.

## 9. Test the full flow (10 min)

1. Open your live URL → see Pricing nav.
2. Click **Buy Pro** → it opens Stripe Checkout.
3. Use Stripe **test mode** first: card `4242 4242 4242 4242`, any future date, any CVC.
4. Complete checkout → Stripe redirects you to `/account`.
5. Check your email — license key should arrive within seconds.
6. Paste the key into Account → status flips to "Pro".
7. Go to `/` → click any model → click **⬇ Excel** → file downloads.

If steps 5–7 fail, check **Stripe → Developers → Webhooks → your endpoint → Events** for delivery errors, and **Replit → Logs** for server errors.

## 10. Flip to live mode

1. Stripe Dashboard top-right: switch from **Test mode** to **Live mode**.
2. Re-create Payment Links in live mode (test-mode links don't carry over).
3. Re-create the webhook endpoint in live mode → copy the new signing secret → update `STRIPE_WEBHOOK_SECRET` in Replit Secrets.
4. Redeploy.
5. Make a real $1 test purchase (or wait for the first real customer — your call).

---

## Optional polish for v1.1

- Verify a sending domain in Resend so emails come from `hello@yourdomain.app`.
- Add OpenGraph image and meta description for nicer social sharing.
- Add a "Save scenario" Pro feature (we already gate it in the pricing copy).
- Branded PDFs for the Team tier (logo upload field on the account page).
- A simple admin page at `/admin` (gated by `ADMIN_PASSWORD`) showing recent purchases.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# Sim2Real — Operations Research Platform
# Sim2Sim — Operations Research Platform

[![Tests](https://github.com/Gavand1969/sim2sim/actions/workflows/ci.yml/badge.svg)](https://github.com/Gavand1969/sim2sim/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/)

**Live demo:** *(deploy on Replit and paste URL here)*
**Live demo:** *(coming soon — deployment in progress)*
**Pricing & launch guide:** see [LAUNCH.md](LAUNCH.md)

PhD-level operations research in your browser — queuing theory, inventory optimization, Monte Carlo simulation, and linear programming with AI-generated insights powered by Claude.

Arena costs $5,000/year. AnyLogic costs $7,000/year. **This is free.**
Arena costs $5,000/year. AnyLogic costs $7,000/year. Sim2Sim is free to use and has an optional one-time **Pro** ($49) or **Team** ($249) upgrade for Excel/PDF export, unlimited AI explanations, advanced inventory models, and commercial-use rights. See [Pricing](static/pricing.html) and [LAUNCH.md](LAUNCH.md) for deployment.

![Sim2Sim Screenshot](docs/screenshot.png)

---

Expand Down
36 changes: 30 additions & 6 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Sim2Real — Operations Research Simulator
Sim2Sim — Operations Research Simulator
Entry point: FastAPI app that serves both the REST API and the static frontend.
"""
import os
Expand All @@ -15,19 +15,22 @@

from src.api.middleware import limiter, add_security_headers
from src.api.routes import router
from src.billing import db as billing_db

load_dotenv()


@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: nothing to initialise beyond imports
# Startup: initialise the billing DB so license activation works
# immediately on first boot (Replit's filesystem persists across runs).
billing_db.init_db()
yield
# Shutdown: nothing to clean up


app = FastAPI(
title="Sim2Real",
title="Sim2Sim",
description="Operations Research Simulator with AI-powered explanations",
version="1.0.0",
docs_url="/api/docs",
Expand All @@ -40,10 +43,20 @@ async def lifespan(app: FastAPI):
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

# ── CORS ──────────────────────────────────────────────────────────────────────
# In production (Replit) the frontend is served from the same origin, so
# wildcard CORS is only needed in local development.
# In production the frontend is served from the same origin, so wildcard
# CORS is only needed in local development. In production we REQUIRE an
# ALLOWED_ORIGIN so we never silently ship with no CORS policy.
_env = os.getenv("ENVIRONMENT", "development")
_origins = ["*"] if _env == "development" else [os.getenv("ALLOWED_ORIGIN", "")]
if _env == "development":
_origins: list[str] = ["*"]
else:
_allowed = os.getenv("ALLOWED_ORIGIN", "").strip()
if not _allowed:
raise RuntimeError(
"ENVIRONMENT=production requires ALLOWED_ORIGIN to be set "
"(comma-separated list of allowed origins)."
)
_origins = [o.strip() for o in _allowed.split(",") if o.strip()]

app.add_middleware(
CORSMiddleware,
Expand All @@ -69,6 +82,17 @@ async def security_headers_middleware(request: Request, call_next):
async def serve_frontend():
return FileResponse("static/index.html")


@app.get("/pricing", include_in_schema=False)
async def serve_pricing():
return FileResponse("static/pricing.html")


@app.get("/account", include_in_schema=False)
async def serve_account():
return FileResponse("static/account.html")


# Catch-all so that client-side navigation always returns index.html
@app.get("/{full_path:path}", include_in_schema=False)
async def catch_all(full_path: str):
Expand Down
44 changes: 44 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[project]
name = "sim2sim"
version = "1.0.0"
description = "PhD-level Operations Research platform — queuing, inventory, simulation, LP, with AI insights."
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [{ name = "Gavin Anderson" }]

[tool.ruff]
line-length = 100
target-version = "py312"
extend-exclude = ["static/**"]

[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes
"I", # isort
"B", # bugbear
"UP", # pyupgrade
"SIM", # simplify
]
ignore = [
"E501", # line too long (we already format thoughtfully)
"E731", # lambda assignment is fine
"B008", # default arg call (FastAPI uses this)
]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["B011", "F401"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q --strict-markers"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.mypy]
python_version = "3.12"
warn_return_any = false
warn_unused_configs = true
ignore_missing_imports = true
exclude = ["static/", "build/", "dist/"]
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ python-dotenv==1.0.1
httpx==0.28.1
pytest==8.3.4
pytest-asyncio==0.24.0
openpyxl==3.1.5
reportlab==4.2.5
Loading
Loading