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
16 changes: 12 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,18 @@ TURNKEY_BASE_URL=https://api.turnkey.com # Turnkey API endpoint (defau
TURNKEY_AGENT_WALLET_ID= # Demo shortcut: Turnkey wallet ID (skips Supabase lookup)
TURNKEY_AGENT_SOLANA_ADDRESS= # Demo shortcut: Solana address for the Turnkey wallet

# x402 Facilitator (payment settlement — optional, Phase 7.M)
X402_FACILITATOR_URL= # Facilitator service endpoint (e.g., http://localhost:4020)
X402_FACILITATOR_FALLBACK_URL= # Fallback facilitator (defaults to PayAI if empty)
X402_FEE_PAYER_ADDRESS= # Fee payer public key for gas sponsorship
# Kora gasless transaction infrastructure (Solana Foundation)
KORA_RPC_URL=https://kora.ozskr.ai # Self-hosted Kora on Railway (used by kora-client.ts)
KORA_API_KEY= # Optional: Kora API key for authenticated endpoints
KORA_HMAC_SECRET= # Optional: Kora HMAC secret for request signing

# x402 facilitator bridge (our Kora-backed endpoint)
# Must be set to the deployed bridge URL — no public fallback is used.
# In production, set to your deployed URL: https://your-app.vercel.app/api/x402
# In development: http://localhost:3000/api/x402
X402_FACILITATOR_URL=http://localhost:3000/api/x402
X402_FACILITATOR_FALLBACK_URL= # Fallback facilitator (e.g., PayAI) if bridge unavailable
X402_FEE_PAYER_ADDRESS= # Informational: Kora acts as fee payer for gasless settlement

# Tapestry Social Graph (Phase 7.T)
TAPESTRY_API_KEY=your-tapestry-api-key # Get from app.usetapestry.dev
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ jobs:
with:
node-version: 20
- run: pnpm install --frozen-lockfile
- name: Build workspace packages
run: pnpm --filter './packages/*' build
- run: pnpm test

build:
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/claude-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,10 @@ jobs:
- Address validation gaps
- Server-side signing attempts
Report findings using the security-auditor format.
allowed_tools: Read,Grep,Glob
model: sonnet

code-quality:
runs-on: ubuntu-latest
continue-on-error: true # remove after this workflow is merged to main
steps:
- uses: actions/checkout@v6
- uses: anthropics/claude-code-action@v1
Expand All @@ -51,5 +50,3 @@ jobs:
- Zod schemas on API boundaries
- Conventional commit messages
Report Pass/Fail with file:line references.
allowed_tools: Read,Grep,Glob
model: haiku
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@solana/kit": "^6.0.1",
"@solana/kora": "^0.1.3",
"@solana/wallet-adapter-backpack": "^0.1.14",
"@solana/wallet-adapter-base": "^0.9.27",
"@solana/wallet-adapter-phantom": "^0.9.28",
Expand Down
20 changes: 14 additions & 6 deletions packages/x402-solana-mcp/src/lib/facilitator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// ---------------------------------------------------------------------------
// Facilitator Integration — CDP Primary + PayAI Fallback
// Facilitator Integration — Kora Bridge Primary + PayAI Fallback
// ---------------------------------------------------------------------------
//
// NOTE: The primary facilitator is now ozskr's own Kora facilitator bridge
// (backed by Solana Foundation's gasless infrastructure). The CDP URL below
// is retained as a legacy fallback constant only — production deployments
// should set X402_FACILITATOR_URL to the Kora bridge endpoint.

/** Settlement result from a facilitator. */
export interface SettlementResult {
Expand All @@ -20,8 +25,10 @@ interface FacilitatorEndpoint {
settlePath: string;
}

/** Default CDP facilitator URL — can be overridden via config. */
const DEFAULT_CDP_URL = 'https://x402.org/facilitator';
/** Legacy fallback facilitator URL — only used when no primary URL is configured.
* Production deployments should set X402_FACILITATOR_URL to the Kora bridge
* (e.g., https://ozskr.ai/api/x402) rather than relying on this fallback. */
const DEFAULT_KORA_FACILITATOR_URL = 'https://facilitator.x402.org'; // legacy fallback only

/** Default PayAI facilitator URL — can be overridden via config. */
const DEFAULT_PAYAI_URL = 'https://facilitator.payai.network';
Expand All @@ -38,7 +45,8 @@ const MAX_RETRIES = 2;
*
* Facilitator URLs are configurable via the `primaryUrl` and `fallbackUrl`
* parameters (sourced from X402_FACILITATOR_URL and X402_FACILITATOR_FALLBACK_URL
* environment variables). Defaults to CDP primary + PayAI fallback.
* environment variables). Defaults to legacy x402.org fallback + PayAI fallback.
* Production: set X402_FACILITATOR_URL to the Kora bridge (e.g., https://ozskr.ai/api/x402).
*
* @param paymentPayload - The signed payment payload (x402 format)
* @param paymentRequirements - The accepted payment requirements
Expand All @@ -53,8 +61,8 @@ export async function submitToFacilitator(
fallbackUrl?: string,
): Promise<SettlementResult> {
const primary: FacilitatorEndpoint = {
name: primaryUrl ? 'custom' : 'cdp',
url: primaryUrl ?? DEFAULT_CDP_URL,
name: primaryUrl ? 'custom' : 'kora',
url: primaryUrl ?? DEFAULT_KORA_FACILITATOR_URL,
settlePath: '/settle',
};

Expand Down
4 changes: 2 additions & 2 deletions packages/x402-solana-mcp/tests/facilitator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ afterEach(() => {
// ---------------------------------------------------------------------------

describe('submitToFacilitator', () => {
it('should succeed on first attempt with CDP', async () => {
it('should succeed on first attempt with Kora', async () => {
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
Expand All @@ -38,7 +38,7 @@ describe('submitToFacilitator', () => {

expect(result.success).toBe(true);
expect(result.transactionSignature).toBe('sig123abc');
expect(result.facilitator).toBe('cdp');
expect(result.facilitator).toBe('kora');
});

it('should fallback to PayAI when CDP fails', async () => {
Expand Down
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 43 additions & 6 deletions src/components/features/landing/waitlist-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

/**
* Waitlist Form
* Email signup with optional wallet address auto-inclusion
* Shows remaining spots out of 500
* Email signup with optional wallet address auto-inclusion.
* Shows remaining spots out of 500 with real-time Supabase Realtime updates —
* the counter decrements live as other users join.
*/

import { useState, useEffect } from 'react';
import { createClient } from '@supabase/supabase-js';
import { useWallet } from '@solana/wallet-adapter-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
Expand All @@ -24,17 +26,51 @@ interface WaitlistFormProps {
source?: string;
}

async function fetchWaitlistCount(): Promise<WaitlistCount> {
const res = await fetch('/api/waitlist/count');
if (!res.ok) throw new Error('count fetch failed');
return res.json() as Promise<WaitlistCount>;
}

export function WaitlistForm({ source }: WaitlistFormProps) {
const { publicKey } = useWallet();
const [email, setEmail] = useState('');
const [state, setState] = useState<FormState>('idle');
const [waitlistData, setWaitlistData] = useState<WaitlistCount | null>(null);

useEffect(() => {
fetch('/api/waitlist/count')
.then((r) => r.json())
.then((data: WaitlistCount) => setWaitlistData(data))
.catch(() => {/* ignore */});
// Initial fetch
fetchWaitlistCount()
.then(setWaitlistData)
.catch(() => {/* silently degrade */});

// Supabase Realtime: subscribe to new waitlist INSERTs and refresh count.
// Uses the public anon key — no auth required.
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) return;

const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: { persistSession: false, autoRefreshToken: false },
});

const channel = supabase
.channel('waitlist-inserts')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'waitlist' },
() => {
// Refresh count from the API (single source of truth)
fetchWaitlistCount()
.then(setWaitlistData)
.catch(() => {/* silently degrade */});
}
)
.subscribe();

return () => {
void supabase.removeChannel(channel);
};
}, []);

const handleSubmit = async (e: React.FormEvent) => {
Expand All @@ -60,6 +96,7 @@ export function WaitlistForm({ source }: WaitlistFormProps) {

if (res.status === 201) {
setState('success');
// Optimistic local update — Realtime will confirm with the true count
setWaitlistData((prev) =>
prev ? { ...prev, count: prev.count + 1, remaining: prev.remaining - 1 } : prev
);
Expand Down
4 changes: 2 additions & 2 deletions src/lib/ai/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ describe('Model Registry', () => {
it('should return correct config for known purpose', () => {
const config = getModelConfig('text-generation');

expect(config.modelId).toBe('claude-sonnet-4-20250514');
expect(config.modelId).toBe('claude-sonnet-4-6');
expect(config.provider).toBe('anthropic');
expect(config.purpose).toContain('Character text');
expect(config.maxTokens).toBe(4096);
expect(config.maxTokens).toBe(1024);
expect(config.temperature).toBe(0.8);
});

Expand Down
2 changes: 1 addition & 1 deletion src/lib/ai/pipeline/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe('Pipeline Stage 4: Generate Content', () => {
expect(result.outputUrl).toBe('https://fal.ai/generated/image123.png');
expect(result.modelUsed).toContain('fal');
expect(result.tokenUsage).toEqual({ input: 0, output: 0, cached: 0 });
expect(result.costUsd).toBe(0.05);
expect(result.costUsd).toBe(0.08);
expect(result.cacheHit).toBe(false);
});

Expand Down
4 changes: 4 additions & 0 deletions src/lib/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { adminCharacters } from './routes/admin-characters';
import { delegation } from './routes/delegation';
import { tapestry } from './routes/tapestry';
import { services } from './routes/services';
import { koraFacilitatorRoutes } from '@/lib/x402/kora-facilitator-bridge';

// App context variables type
type AppVariables = {
Expand Down Expand Up @@ -117,6 +118,9 @@ app.route('/admin-issues', adminIssues);
app.route('/admin-report', adminReport);
app.route('/delegation', delegation);
app.route('/tapestry', tapestry);
// Kora facilitator bridge — implements x402 facilitator protocol over Kora gasless infra
// Must be mounted BEFORE /services so HTTPFacilitatorClient can reach /api/x402/*
app.route('/x402', koraFacilitatorRoutes);
app.route('/services', services);

// Global error handler with AppError support
Expand Down
6 changes: 3 additions & 3 deletions src/lib/social/publisher-factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ vi.mock('@/lib/utils/logger', () => ({

import { getPublisher, isPublishingEnabled, resetPublisherCache } from './publisher-factory';
import { AyrshareAdapter } from './ayrshare-adapter';
import { TwitterAdapter } from './twitter-adapter';
import { XDirectAdapter } from './x-direct-adapter';

describe('publisher-factory', () => {
beforeEach(() => {
Expand All @@ -47,15 +47,15 @@ describe('publisher-factory', () => {
expect(publisher.provider).toBe(SocialProvider.AYRSHARE);
});

it('should return TwitterAdapter for DIRECT provider', () => {
it('should return XDirectAdapter for DIRECT provider', () => {
mockGetFeatureFlags.mockReturnValue({
socialPublishEnabled: true,
socialPublishProvider: SocialProvider.DIRECT,
});

const publisher = getPublisher();

expect(publisher).toBeInstanceOf(TwitterAdapter);
expect(publisher).toBeInstanceOf(XDirectAdapter);
expect(publisher.provider).toBe(SocialProvider.DIRECT);
});

Expand Down
Loading
Loading