Skip to content
This repository was archived by the owner on Jun 19, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7d5ecb8
feat: add predict page
random-anon333 Apr 4, 2026
352efee
design is pretty
random-anon333 Apr 4, 2026
1fa590c
a lot of tweats and changes
random-anon333 Apr 4, 2026
9ece89f
moar pretify
random-anon333 Apr 4, 2026
9bdc946
more monster
random-anon333 Apr 4, 2026
aefe5f7
more monster
random-anon333 Apr 4, 2026
68c6915
add better questions
random-anon333 Apr 4, 2026
b7dfb15
add better questions
random-anon333 Apr 4, 2026
24e0de4
add better questions
random-anon333 Apr 4, 2026
ddddd32
add frontend stripe integration
random-anon333 Apr 4, 2026
f9b05e9
lookign good
random-anon333 Apr 4, 2026
eafeea2
lookign very very good
random-anon333 Apr 4, 2026
32dc07c
coins work
random-anon333 Apr 4, 2026
f4b08a9
predictions coming toegther
random-anon333 Apr 4, 2026
4991609
predictions coming toegther
random-anon333 Apr 4, 2026
18218bb
predictions coming toegther
random-anon333 Apr 4, 2026
43fdf99
predict prototyle works
random-anon333 Apr 4, 2026
f0c4e30
predict prototyle works
random-anon333 Apr 4, 2026
06fee88
fix chart
random-anon333 Apr 4, 2026
f1d3dd6
fix chart
random-anon333 Apr 4, 2026
56ce3f8
fix meaning on compatibility
random-anon333 Apr 4, 2026
86e711c
more pretifly
random-anon333 Apr 4, 2026
4549012
try to fix ci problem
random-anon333 Apr 4, 2026
adeae4f
pretify signin
random-anon333 Apr 4, 2026
5e58374
okay, lookign good
random-anon333 Apr 4, 2026
c309ce0
different colors for wheels
random-anon333 Apr 4, 2026
8afabf8
adjust compatibility - its super stupid, but for now before ai its okay
random-anon333 Apr 4, 2026
a2367f3
adjust compatibility - its super stupid, but for now before ai its okay
random-anon333 Apr 4, 2026
e1e1f45
quick fix makefile
random-anon333 Apr 5, 2026
d128f5c
makefile
random-anon333 Apr 9, 2026
3421cb7
feat: add bet footing
random-anon333 Apr 14, 2026
ac6eb77
feat: improve banner
random-anon333 Apr 14, 2026
7a6b37c
feat: add info for e2e prediction
random-anon333 Apr 14, 2026
06edb64
feat: now it's either add ai or nevermind
random-anon333 Apr 14, 2026
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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,9 @@ AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
AUTH_GITHUB_ID=
AUTH_GITHUB_SECRET=

# Stripe (optional) — astro coin purchases on /dashboard
# Dashboard: https://dashboard.stripe.com/apikeys
STRIPE_SECRET_KEY=
# Webhook signing secret (Stripe CLI local: stripe listen --forward-to localhost:8066/api/stripe/webhook)
STRIPE_WEBHOOK_SECRET=
22 changes: 19 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
.PHONY: dev server generate build start lint typecheck test coverage check install clean
.PHONY: dev server generate migrate migrate-dev predict-build build start lint typecheck test coverage check install clean

# Development server (alias: dev)
# Development server. Stops whatever is bound to 8066 (usually a stray `next dev`)
# and clears a stale Turbopack lock so a fresh server can start.
dev:
@for pid in $$(lsof -ti:8066 2>/dev/null); do kill $$pid 2>/dev/null || true; done
@sleep 0.4
@rm -f .next/dev/lock
npm run dev

# Prisma
generate:
npx prisma generate

# Generate predict runtime JSON from source strings.
predict-build:
npm run predict:build

# Apply pending migrations (production / CI). Requires DIRECT_URL or DATABASE_URL in .env.
migrate:
npx prisma migrate deploy

# Create/apply migrations in development (interactive). Requires DIRECT_URL or DATABASE_URL.
migrate-dev:
npx prisma migrate dev

# Build and run
build:
npx prisma generate
npm run build

server:
server:build
npm run start

# Lint and typecheck
Expand Down
57 changes: 55 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,59 @@
# 🔮 future
# Future AI

*Natal charts — Western astrology, whole sign houses, tropical zodiac.*

Next.js app: birth date, time, and place → planetary positions, houses, chart wheel, and placement tables.

## Quick start

```bash
make install
make dev
```

Open [http://localhost:8066](http://localhost:8066).

## Development vs production

| Command | What it does |
|--------|----------------|
| `make dev` | `next dev` — serves **current source** (hot reload). |
| `make build` | Production build into `.next` (runs `prisma generate` first). |
| `make server` | `next start` — serves the **last** production build only. |

`make server` does not compile your latest edits. Run `make build` before `make server` whenever you want production mode to match recent changes:

```bash
make build && make server
```

Both dev and production use **port 8066**; only one can listen at a time.

## Common commands

- `make dev` — development server
- `make build` — production build
- `make server` — run production server (after `make build`)
- `make lint` / `make typecheck` / `make test` — quality checks (`make check` runs all three)
- `make generate` — Prisma client from schema
- `make migrate` — apply migrations (needs `DATABASE_URL` / `DIRECT_URL` in `.env`)
- `make migrate-dev` — create/apply migrations in development
- `make clean` — remove `.next` and `node_modules`

Stripe and Astro Coins setup: [docs/STRIPE.md](docs/STRIPE.md).

## Stack

Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS 4, Prisma, Vitest.

<br>

---

## TODO

<br>

### *"ancient wisdom meets the AI age"*
- training / fine-tuning the AI for astro
- infra, deplopyment, etc.

15 changes: 0 additions & 15 deletions TODO.md

This file was deleted.

39 changes: 39 additions & 0 deletions app/api/astro-coins/ledger/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NextResponse } from 'next/server';
import { auth } from '@/auth';
import { prisma } from '@/lib/db';

const DEFAULT_LIMIT = 50;
const MAX_LIMIT = 100;

export async function GET(request: Request) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const { searchParams } = new URL(request.url);
const rawLimit = searchParams.get('limit');
let limit = DEFAULT_LIMIT;
if (rawLimit != null) {
const n = Number.parseInt(rawLimit, 10);
if (Number.isFinite(n) && n > 0) {
limit = Math.min(n, MAX_LIMIT);
}
}

const entries = await prisma.astroCoinLedger.findMany({
where: { userId: session.user.id },
orderBy: { createdAt: 'desc' },
take: limit,
select: {
id: true,
delta: true,
balanceAfter: true,
reason: true,
refId: true,
createdAt: true,
},
});

return NextResponse.json({ entries });
}
32 changes: 32 additions & 0 deletions app/api/astro-coins/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { NextResponse } from 'next/server';
import { auth } from '@/auth';
import { prisma } from '@/lib/db';
import { getStripeSecretKey } from '@/lib/stripe-server';

export const dynamic = 'force-dynamic';

export async function GET() {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

try {
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: { astroCoins: true },
});

if (!user) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}

return NextResponse.json({
coins: user.astroCoins,
stripeEnabled: Boolean(getStripeSecretKey()),
});
} catch (err) {
console.error('[astro-coins] GET', err);
return NextResponse.json({ error: 'Database error' }, { status: 500 });
}
}
2 changes: 1 addition & 1 deletion app/api/charts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function POST(request: Request) {
typeof body.label === 'string'
? body.label.trim()
: isPrimary
? 'My chart'
? 'my chart'
: '';
const birthData = body.birthData;
const chartResult = body.chartResult;
Expand Down
7 changes: 6 additions & 1 deletion app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ export async function POST(request: Request) {

if (!res.ok) {
const err = await res.text();
console.error('Anthropic API error', res.status, err);
const requestId = res.headers.get('request-id') ?? res.headers.get('x-request-id');
console.error('Anthropic API error', {
status: res.status,
requestId,
details: process.env.NODE_ENV === 'development' ? err : undefined,
});
return NextResponse.json(
{
error:
Expand Down
96 changes: 96 additions & 0 deletions app/api/predict/bet/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { NextResponse } from 'next/server';
import { auth } from '@/auth';
import {
applyAstroCoinDelta,
InsufficientAstroCoinsError,
} from '@/lib/astro-coins-ledger';
import { isValidPredictQuestionId, normalizePredictBetSide } from '@/lib/predict-validate';
import { prisma } from '@/lib/db';

const MAX_COINS_PER_BET = 1_000_000;

export async function POST(request: Request) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
}

const questionId =
typeof body === 'object' &&
body !== null &&
'questionId' in body &&
typeof (body as { questionId: unknown }).questionId === 'number'
? (body as { questionId: number }).questionId
: Number.NaN;

const sideRaw =
typeof body === 'object' &&
body !== null &&
'side' in body &&
typeof (body as { side: unknown }).side === 'string'
? (body as { side: string }).side.trim()
: '';

const coinsRaw =
typeof body === 'object' &&
body !== null &&
'coins' in body &&
typeof (body as { coins: unknown }).coins === 'number'
? (body as { coins: number }).coins
: Number.NaN;

const coins = Math.floor(Number(coinsRaw));
if (!Number.isFinite(coins) || coins < 1 || coins > MAX_COINS_PER_BET) {
return NextResponse.json(
{ error: `Invest between 1 and ${MAX_COINS_PER_BET} coins` },
{ status: 400 },
);
}

const normalizedSide = normalizePredictBetSide(questionId, sideRaw);
if (normalizedSide === null || !isValidPredictQuestionId(questionId)) {
return NextResponse.json(
{ error: 'Unknown question or invalid choice' },
{ status: 400 },
);
}

const refId = `${questionId}:${normalizedSide}`;

try {
const balance = await prisma.$transaction(async tx => {
const afterDebit = await applyAstroCoinDelta(
tx,
session.user.id,
-coins,
'predict_bet',
refId,
);
await tx.predictBet.create({
data: {
userId: session.user.id,
questionId,
side: normalizedSide,
coins,
},
});
return afterDebit;
});
return NextResponse.json({ balance });
} catch (e) {
if (e instanceof InsufficientAstroCoinsError) {
return NextResponse.json(
{ error: 'Not enough astro coins' },
{ status: 402 },
);
}
throw e;
}
}
Loading
Loading