Skip to content
Open
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
168 changes: 168 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
---
name: AxiomID Agentic
colors:
primary: "#00FF41"
secondary: "#00D4FF"
accent: "#FBBF24"
success: "#16A34A"
warning: "#D97706"
danger: "#DC2626"
background: "#060608"
surface: "#09090B"
foreground: "#F4F4F6"
muted: "#121217"
border: "rgba(0, 255, 65, 0.08)"
typography:
h1:
fontFamily: "Geist"
fontSize: 2.5rem
h2:
fontFamily: "Geist"
fontSize: 1.75rem
body-md:
fontFamily: "Geist"
fontSize: 1rem
body-sm:
fontFamily: "Geist"
fontSize: 0.875rem
label-caps:
fontFamily: "Geist Mono"
fontSize: 0.75rem
textTransform: uppercase
letterSpacing: 0.05em
sourceScale: "14/16/18/24/32/40"
weights: "300, 400, 500, 600, 700, 800, 900"
rounded:
sm: 8px
md: 14px
lg: 20px
full: 9999px
spacing:
xs: 4px
sm: 8px
md: 16px
lg: 24px
xl: 32px
sourceScale: "8pt baseline grid"
---

## Overview

Conversational AI-first interface with minimal controls, clear outcomes, and delegated task flows for agentic workflows. Dark cyberpunk aesthetic with neon green accents and glass-morphism surfaces.

## Style Foundations

- **Visual style:** dark, modern, cyberpunk, glass-morphism
- **Typography scale:** 14/16/18/24/32/40
- **Typography fonts:** primary=Geist, display=Geist, mono=Geist Mono
- **Typography weights:** 300, 400, 500, 600, 700, 800, 900
- **Color palette:** dark background (#060608), neon green primary (#00FF41), electric blue secondary (#00D4FF), gold accent (#FBBF24)
- **Spacing scale:** 8pt baseline grid
- **Surfaces:** glass-panel with backdrop-filter blur(12px), semi-transparent borders
- **Glow effects:** text-shadow and box-shadow with rgba primary color glow

## Colors

- **Background (#060608):** Deep near-black base.
- **Surface (#09090B):** Slightly lighter card/panel surfaces.
- **Foreground (#F4F4F6):** Near-white text on dark backgrounds.
- **Primary (#00FF41):** Neon green — interactive elements, active states, hover glows.
- **Secondary (#00D4FF):** Electric blue — secondary actions, gradients.
- **Accent (#FBBF24):** Gold — highlights, badges, premium indicators.
- **Success (#16A34A):** Green status indicators.
- **Warning (#D97706):** Amber warning signals.
- **Danger (#DC2626):** Red destructive actions and error states.
- **Muted (#121217):** Subtle background layers, dividers.
- **Border (rgba(0, 255, 65, 0.08)):** Subtle green-tinted borders.

## Typography

- **Headings:** Geist (sans-serif), bold weights (600-800)
- **Body:** Geist (sans-serif), weight 400
- **Mono:** Geist Mono for code, labels, and data displays
- **Label caps:** 0.75rem, uppercase, 0.05em letter-spacing, Geist Mono

## Spacing

- **Grid:** 8pt baseline grid
- **Card padding:** 20-24px
- **Section gaps:** 32-48px
- **Component gaps:** 12-16px

## Rounded Corners

- **Cards (bento):** 20px radius with glass border
- **Buttons:** 12px radius
- **Inputs:** 10px radius
- **Pills/badges:** 9999px (full rounded)

## Components

### Bento Card
- Background: rgba(9, 9, 11, 0.75) with backdrop-filter blur(16px)
- Border: 1px solid rgba(0, 255, 65, 0.08)
- Border-radius: 20px
- Top gradient line on hover: linear-gradient(90deg, transparent, #00FF41, #00D4FF, transparent)
- Hover: translateY(-2px), green glow shadow, border opacity increase

### Primary Button
- Gradient background: rgba(0, 255, 65, 0.15) to rgba(0, 212, 255, 0.15)
- Hover: solid #00FF41 to #00D4FF gradient, black text
- Text: 0.75rem uppercase, Geist Mono, 0.05em letter-spacing

### Ghost Button
- Background: rgba(255, 255, 255, 0.02)
- Border: 1px solid rgba(255, 255, 255, 0.05)
- Hover: white text, brighter border

### Engineering Grid Background
- Background pattern: 40px grid with subtle white lines at 1.5% opacity
- Mask: radial gradient at center (black 30%, transparent 95%)
- Scanline: animated 4px top-to-bottom purple gradient line

### Glow Text
- .text-neon: #00FF41 with 10px green text-shadow
- .text-electric: #00D4FF with 10px blue text-shadow

### Scrollbar
- Width: 5px
- Track: #030305
- Thumb: #1F1F2E, rounded, hover turns green (#00FF41)

## Animation

- Card hover: 0.5s cubic-bezier(0.16, 1, 0.3, 1)
- Button hover: 0.3s cubic-bezier(0.16, 1, 0.3, 1)
- Scanline: 8s linear infinite
- Pulse-slow: 3s cubic-bezier(0.4, 0, 0.6, 1) infinite

## Accessibility

- Contrast ratio minimum 4.5:1 for text
- Focus indicators with green glow outline
- Interactive elements have visible hover/focus states
- Reduced motion: respect prefers-reduced-motion

## Writing Tone

- Technical, concise, Arabic-supportive
- English UI labels, Arabic content where appropriate
- Short labels, clear CTAs

## Rules: Do

- Use dark theme as default (color-scheme: dark)
- Use neon green (#00FF41) for primary CTAs and active states
- Use glass-morphism for cards and panels
- Apply backdrop-filter blur on overlay surfaces
- Use Geist Mono for code, labels, technical data
- Use gradient borders on hover for interactive cards
- Keep generous whitespace and breathing room

## Rules: Don't

- Don't use pure white backgrounds (#FFFFFF) on dark surfaces
- Don't use bright saturated colors without alpha for backgrounds
- Don't add box-shadows without considering glow effects
- Don't use non-Geist font families
- Don't create heavy/dense layouts — prefer bento grid with whitespace
4 changes: 4 additions & 0 deletions apps/axiomid/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ PI_WALLET_PRIVATE_SEED=
# Set to `true` to point the Pi SDK at the sandbox environment.
NEXT_PUBLIC_PI_SANDBOX=false

# 5. Development URL for Pi Sandbox communication
# Set to your sandbox subdomain from Pi Developer Portal (e.g., axiomid8992.pinet.com)
NEXT_PUBLIC_DEV_URL=

# -----------------------------------------------------------------------------
# Web3Auth (client)
# -----------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions apps/axiomid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@pinetwork/pi-sdk-js": "^2.0.0",
"@prisma/client": "^6.0.0",
"framer-motion": "^12.38.0",
"next": "16.2.6",
Expand Down
31 changes: 0 additions & 31 deletions apps/axiomid/src/app/api/__tests__/auth-connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ jest.mock('@/lib/prisma', () => ({
},
}));

jest.mock('@/lib/tiers', () => ({
calculateTier: jest.fn(),
}));

import { POST } from '../auth/connect/route';

describe('POST /api/auth/connect', () => {
Expand Down Expand Up @@ -66,33 +62,6 @@ describe('POST /api/auth/connect', () => {
expect(prisma.user.create).toHaveBeenCalled();
});

it('should return existing user and update tier if necessary', async () => {
const existingUser = {
walletAddress: mockWalletAddress,
xp: 150,
tier: 'Ghost',
actions: [],
};

(prisma.user.findUnique as any).mockResolvedValue(existingUser);
(calculateTier as any).mockReturnValue('Spark');
(prisma.user.update as any).mockResolvedValue({
...existingUser,
tier: 'Spark',
});

const req = {
json: async () => ({ walletAddress: mockWalletAddress }),
} as Request;

const res = await POST(req);
const data = await res.json();

expect(res.status).toBe(200);
expect(data.user.tier).toBe('Spark');
expect(prisma.user.update).toHaveBeenCalled();
});

it('should return 500 on internal error', async () => {
(prisma.user.findUnique as any).mockRejectedValue(new Error('DB Error'));

Expand Down
2 changes: 1 addition & 1 deletion apps/axiomid/src/app/api/agent/activate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export async function POST(request: Request) {
const agent = await prisma.userAgent.update({
where: { userId: user.id },
data: {
status: 'active',
status: 'ACTIVE',
lastActive: new Date(),
},
select: {
Expand Down
4 changes: 2 additions & 2 deletions apps/axiomid/src/app/api/agent/main/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function POST(request: Request) {
if (existing) {
const updated = await prisma.userAgent.update({
where: { userId: user.id },
data: { name: name || existing.name, status: 'active', lastActive: new Date() },
data: { name: name || existing.name, status: 'ACTIVE', lastActive: new Date() },
select: agentSelect,
});

Expand Down Expand Up @@ -78,7 +78,7 @@ export async function POST(request: Request) {
data: {
userId: user.id,
name: name || 'My Agent',
status: 'active',
status: 'ACTIVE',
apiKeyHash,
permissions: JSON.stringify(['claim', 'verify']),
lastActive: new Date(),
Expand Down
2 changes: 1 addition & 1 deletion apps/axiomid/src/app/api/agent/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export async function POST(request: Request) {
data: {
userId: user.id,
name: name || 'My Agent',
status: 'inactive',
status: 'INACTIVE',
apiKeyHash,
permissions: JSON.stringify(['claim', 'verify']),
},
Expand Down
15 changes: 15 additions & 0 deletions apps/axiomid/src/app/api/pi/payment/cancel/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server";

export async function POST(request: Request) {
try {
const { paymentId } = await request.json();
if (!paymentId) {
return NextResponse.json({ error: "paymentId is required" }, { status: 400 });
}

return NextResponse.json({ success: true, paymentId });
} catch (error) {
console.error("Error cancelling payment:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
15 changes: 15 additions & 0 deletions apps/axiomid/src/app/api/pi/payment/error/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server";

export async function POST(request: Request) {
try {
const { paymentId, error: errorMsg } = await request.json();
if (!paymentId || !errorMsg) {
return NextResponse.json({ error: "paymentId and error are required" }, { status: 400 });
}

return NextResponse.json({ success: true });
} catch (error) {
console.error("Error logging payment error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
36 changes: 36 additions & 0 deletions apps/axiomid/src/app/api/pi/payment/incomplete/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NextResponse } from "next/server";
import { getPiEnv } from "@/lib/pi/env";

export async function POST(request: Request) {
try {
const { paymentId, transactionId } = await request.json();
if (!paymentId) {
return NextResponse.json({ error: "paymentId is required" }, { status: 400 });
}

const { apiKey } = getPiEnv();

const response = await fetch(
`https://api.minepi.com/v2/payments/${paymentId}`,
{
headers: {
Authorization: `Key ${apiKey}`,
},
}
);
Comment on lines +13 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a timeout to the outbound Minepi fetch in apps/axiomid/src/app/api/pi/payment/incomplete/route.ts.

The handler performs a blocking external call to https://api.minepi.com/v2/payments/${paymentId} without any AbortSignal/timeout, so upstream slowness can tie up request handling and cascade latency.

🔧 Suggested fix
-    const response = await fetch(
-      `https://api.minepi.com/v2/payments/${paymentId}`,
-      {
-        headers: {
-          Authorization: `Key ${apiKey}`,
-        },
-      }
-    );
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 8000);
+    const response = await fetch(
+      `https://api.minepi.com/v2/payments/${paymentId}`,
+      {
+        headers: {
+          Authorization: `Key ${apiKey}`,
+        },
+        signal: controller.signal,
+      }
+    ).finally(() => clearTimeout(timeout));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await fetch(
`https://api.minepi.com/v2/payments/${paymentId}`,
{
headers: {
Authorization: `Key ${apiKey}`,
},
}
);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
const response = await fetch(
`https://api.minepi.com/v2/payments/${paymentId}`,
{
headers: {
Authorization: `Key ${apiKey}`,
},
signal: controller.signal,
}
).finally(() => clearTimeout(timeout));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/axiomid/src/app/api/pi/payment/incomplete/route.ts` around lines 13 -
20, The outbound fetch to `https://api.minepi.com/v2/payments/${paymentId}` has
no timeout/AbortSignal; wrap the call with an AbortController, pass
controller.signal into the fetch options (where Authorization header is set),
create a timer (e.g. const timeout = setTimeout(() => controller.abort(), 5000))
before the fetch and clear it after the response (clearTimeout(timeout)), and
handle the abort case by catching the thrown DOMException/AbortError and
returning an appropriate error response; update the fetch invocation in the
route handler where `fetch(..., { headers: { Authorization: \`Key ${apiKey}\` }
})` appears to include `signal: controller.signal`.


if (!response.ok) {
return NextResponse.json(
{ error: "Failed to fetch incomplete payment" },
{ status: response.status }
);
}

const paymentData = await response.json();

return NextResponse.json({ success: true, payment: paymentData });
Comment on lines +13 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Incomplete payment is fetched but never approved or completed

The Pi Network server-side flow for an incomplete payment requires calling /v2/payments/{id}/approve and then /v2/payments/{id}/complete (with the on-chain txid). This handler only performs a GET to fetch the payment status and returns the result — it never calls approve or complete. Any payment surfaced by the SDK as incomplete during pi.connect() will remain permanently stuck in the pending state, as neither the app nor the SDK will make further attempts to resolve it.

Fix in Conductor

} catch (error) {
console.error("Error handling incomplete payment:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
12 changes: 12 additions & 0 deletions apps/axiomid/src/app/context/sandbox-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";

import { useEffect } from "react";
import { initSandboxCompatibility } from "@/lib/pi-sandbox";

export function SandboxProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
initSandboxCompatibility();
}, []);

return <>{children}</>;
}
Comment on lines +1 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security window.postMessage is patched unconditionally for every visitor

SandboxProvider wraps the entire app and always calls initSandboxCompatibility(), which replaces window.postMessage with a version that forwards every outgoing message to window.parent with "*" as targetOrigin whenever the app is inside an iframe (see pi-sandbox.ts lines 4–10). This runs for all users regardless of whether sandbox mode is active, broadcasting any postMessage sent in an iframe context — including auth tokens or other app state — to an arbitrary parent origin. Consider gating this behind the same sandbox flag used elsewhere.

Fix in Conductor

Loading