diff --git a/apps/bootstrapper/.env.example b/apps/bootstrapper/.env.example
new file mode 100644
index 00000000..7abac93c
--- /dev/null
+++ b/apps/bootstrapper/.env.example
@@ -0,0 +1,15 @@
+# Environment Configuration
+NODE_ENV=development
+
+# Database
+DATABASE_URL=./data/bootstrapper.db
+
+# Nostr Configuration
+NOSTR_BOOTSTRAPPER_PRIVATE_KEY=
+
+# Server Configuration
+PORT=8001
+HOST=0.0.0.0
+
+# Logging
+LOG_LEVEL=info
diff --git a/apps/bootstrapper/.gitignore b/apps/bootstrapper/.gitignore
new file mode 100644
index 00000000..7b0c46c6
--- /dev/null
+++ b/apps/bootstrapper/.gitignore
@@ -0,0 +1,92 @@
+# Dependencies
+node_modules/
+
+# SvelteKit build outputs
+build/
+dist/
+.svelte-kit/
+
+# Temporary folders
+tmp/
+temp/
+
+# Logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Environment files
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# Database
+*.db
+*.sqlite
+*.sqlite3
+data/
+
+# IDE and editor files
+.vscode/
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.swp
+*.swo
+*~
+
+# OS generated files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# Coverage directory used by tools like istanbul
+coverage/
+*.lcov
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# Yarn
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# Vercel
+.vercel
+
+# Playwright
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
+
+# Vitest
+coverage/
+
+# Custom application files
+/static/uploads/
+*.key
+*.crt
+*.pem
diff --git a/apps/bootstrapper/.prettierrc b/apps/bootstrapper/.prettierrc
new file mode 100644
index 00000000..7e0dcde7
--- /dev/null
+++ b/apps/bootstrapper/.prettierrc
@@ -0,0 +1,24 @@
+{
+ "useTabs": false,
+ "tabWidth": 4,
+ "singleQuote": true,
+ "semi": true,
+ "trailingComma": "es5",
+ "printWidth": 100,
+ "plugins": ["prettier-plugin-svelte"],
+ "pluginSearchDirs": ["."],
+ "overrides": [
+ {
+ "files": "*.svelte",
+ "options": {
+ "parser": "svelte",
+ "useTabs": false,
+ "tabWidth": 4,
+ "singleQuote": true,
+ "semi": true,
+ "trailingComma": "es5",
+ "printWidth": 100
+ }
+ }
+ ]
+}
diff --git a/apps/bootstrapper/app.postcss b/apps/bootstrapper/app.postcss
new file mode 100644
index 00000000..b5c61c95
--- /dev/null
+++ b/apps/bootstrapper/app.postcss
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/apps/bootstrapper/package.json b/apps/bootstrapper/package.json
new file mode 100644
index 00000000..9a87a9f2
--- /dev/null
+++ b/apps/bootstrapper/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "satshoot-bootstrapper",
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev --host 0.0.0.0 --port 8001",
+ "build": "vite build",
+ "preview": "vite preview --port 8001",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+ "test": "vitest",
+ "test:e2e": "playwright test",
+ "lint": "eslint .",
+ "format": "prettier --write ."
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.47.0",
+ "@skeletonlabs/skeleton": "^3.1.2",
+ "@skeletonlabs/skeleton-svelte": "^1.2.1",
+ "@sveltejs/adapter-node": "^5.2.7",
+ "@sveltejs/kit": "^2.20.7",
+ "@sveltejs/vite-plugin-svelte": "^5.0.3",
+ "@tailwindcss/forms": "^0.5.10",
+ "@tailwindcss/typography": "^0.5.16",
+ "@tailwindcss/vite": "^4.1.4",
+ "@types/node": "^20.17.30",
+ "@types/ws": "^8.5.12",
+ "eslint": "^9.13.0",
+ "prettier": "^3.6.2",
+ "prettier-plugin-svelte": "^3.2.6",
+ "svelte": "^5.27.1",
+ "svelte-check": "^4.1.6",
+ "tailwindcss": "^4.1.4",
+ "tslib": "^2.8.1",
+ "typescript": "^5.8.3",
+ "vite": "^6.3.1",
+ "vitest": "^3.1.1"
+ },
+ "dependencies": {
+ "@nostr-dev-kit/ndk": "workspace:^",
+ "@nostr-dev-kit/ndk-svelte": "workspace:^",
+ "@sveltejs/adapter-static": "^3.0.8",
+ "nostr-tools": "~2.5.2",
+ "svelte-persisted-store": "^0.11.0",
+ "ws": "^8.18.0"
+ }
+}
diff --git a/apps/bootstrapper/src/app.css b/apps/bootstrapper/src/app.css
new file mode 100644
index 00000000..7aa60aa3
--- /dev/null
+++ b/apps/bootstrapper/src/app.css
@@ -0,0 +1,11 @@
+/* Base styles */
+html,
+body {
+ @apply h-full overflow-hidden;
+}
+
+.app {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
diff --git a/apps/bootstrapper/src/app.d.ts b/apps/bootstrapper/src/app.d.ts
new file mode 100644
index 00000000..06e03f2e
--- /dev/null
+++ b/apps/bootstrapper/src/app.d.ts
@@ -0,0 +1,14 @@
+// See https://kit.svelte.dev/docs/types#app
+
+// for information about these interfaces
+declare global {
+ namespace App {
+ // interface Error {}
+ // interface Locals {}
+ // interface PageData {}
+ // interface PageState {}
+ // interface Platform {}
+ }
+}
+
+export {};
diff --git a/apps/bootstrapper/src/app.html b/apps/bootstrapper/src/app.html
new file mode 100644
index 00000000..1141bc4e
--- /dev/null
+++ b/apps/bootstrapper/src/app.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ SatShoot Bootstrapper
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/apps/bootstrapper/src/app.js b/apps/bootstrapper/src/app.js
new file mode 100644
index 00000000..7ef27151
--- /dev/null
+++ b/apps/bootstrapper/src/app.js
@@ -0,0 +1,2 @@
+import '@tailwindcss/browser';
+import './app.css';
diff --git a/apps/bootstrapper/src/routes/+layout.svelte b/apps/bootstrapper/src/routes/+layout.svelte
new file mode 100644
index 00000000..d310ef4f
--- /dev/null
+++ b/apps/bootstrapper/src/routes/+layout.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/apps/bootstrapper/src/routes/+page.svelte b/apps/bootstrapper/src/routes/+page.svelte
new file mode 100644
index 00000000..23a72cab
--- /dev/null
+++ b/apps/bootstrapper/src/routes/+page.svelte
@@ -0,0 +1,234 @@
+
+
+
+
+
+
+
+
+ 🚀
+
+
+ Bootstrapper Account Service
+
+
+ Advanced Nostr Job Management with Intelligent Bid Resolution
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🔗
+
+
Connection Status
+
+
+
+
+
+
+
+
+
+
+
+ ⚡
+
+
Quick Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ⚙️
+
+
Configuration
+
+
+
+
+ Bid Timeout
+ 5 minutes
+
+
+
+
Auto Resolution
+
+
+ Enabled
+
+
+
+
+ Min Reputation
+ 0
+
+
+
+ Active Relays
+ 1
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/bootstrapper/svelte.config.js b/apps/bootstrapper/svelte.config.js
new file mode 100644
index 00000000..e8852be0
--- /dev/null
+++ b/apps/bootstrapper/svelte.config.js
@@ -0,0 +1,46 @@
+import adapter from '@sveltejs/adapter-static';
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ // Consult https://kit.svelte.dev/docs/integrations#preprocessors
+ // for more information about preprocessors
+ // This is needed for vite to handle Typescript in svelte components
+ preprocess: vitePreprocess({ script: true }),
+
+ vite: {
+ server: {
+ fs: {
+ allow: ['./packages/ndk'],
+ },
+ },
+ },
+
+ kit: {
+ version: {
+ // Every 10 secs this checks if app was updated
+ // updated means build timestamp checking by default
+ // see: https://kit.svelte.dev/docs/configuration#version
+ // pollInterval: 10000
+ },
+ adapter: adapter(
+ // ---------------- For build ------------------
+ {
+ pages: 'build/htdocs',
+ assets: 'build/htdocs',
+ // For SPA this is important. If a dynamic route is requested on a static site,
+ // a fallback page is the response which svelte recognizes on the client-side
+ // and tries to do client-side dynamic routing. Hosting provider specific option.
+
+ // For test deploy use commented fallback page
+ // fallback: 'index.html',
+ fallback: 'index.html',
+ precompress: false,
+ // strict is needed to check if all sites have prerender = true OR have a fallback page(see above)
+ strict: true,
+ }
+ ),
+ },
+};
+
+export default config;
diff --git a/apps/bootstrapper/tsconfig.json b/apps/bootstrapper/tsconfig.json
new file mode 100644
index 00000000..834d87d9
--- /dev/null
+++ b/apps/bootstrapper/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "moduleResolution": "bundler",
+ "types": ["vitest/globals"]
+ }
+ // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
+ //
+ // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
+ // from the referenced tsconfig.json - TypeScript does not merge them in
+}
diff --git a/apps/bootstrapper/vite.config.ts b/apps/bootstrapper/vite.config.ts
new file mode 100644
index 00000000..6e10f833
--- /dev/null
+++ b/apps/bootstrapper/vite.config.ts
@@ -0,0 +1,18 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+import tailwindcss from '@tailwindcss/vite';
+
+export default defineConfig({
+ plugins: [sveltekit(), tailwindcss()],
+ server: {
+ port: 8001,
+ host: '0.0.0.0',
+ },
+ preview: {
+ port: 8001,
+ host: '0.0.0.0',
+ },
+ test: {
+ include: ['src/**/*.{test,spec}.{js,ts}'],
+ },
+});
diff --git a/apps/satshoot/src/lib/components/Onboarding/PendingOnboardingJobs.svelte b/apps/satshoot/src/lib/components/Onboarding/PendingOnboardingJobs.svelte
new file mode 100644
index 00000000..5d37cfe5
--- /dev/null
+++ b/apps/satshoot/src/lib/components/Onboarding/PendingOnboardingJobs.svelte
@@ -0,0 +1,58 @@
+
+
+
+
⭐ Onboarding ⭐
+
Complete the following jobs to earn rewards on SatShoot:
+ {#each Array.from(jobList) as job (job.id)}
+
+ {/each}
+
diff --git a/apps/satshoot/src/lib/events/OnboardingJobEvent.ts b/apps/satshoot/src/lib/events/OnboardingJobEvent.ts
new file mode 100644
index 00000000..46ac9ece
--- /dev/null
+++ b/apps/satshoot/src/lib/events/OnboardingJobEvent.ts
@@ -0,0 +1,84 @@
+import { NDKEvent, type NDKTag, type NostrEvent } from '@nostr-dev-kit/ndk';
+import NDK from '@nostr-dev-kit/ndk';
+import { NDKKind } from '@nostr-dev-kit/ndk';
+
+export enum OnboardingJobStatus {
+ New = 0,
+ InProgress = 1,
+ Resolved = 2,
+ Failed = 3,
+}
+
+export class OnboardingJobEvent extends NDKEvent {
+ private _status: OnboardingJobStatus;
+ private _title: string;
+
+ constructor(ndk?: NDK, rawEvent?: NostrEvent) {
+ super(ndk, rawEvent);
+ this.kind ??= NDKKind.FreelanceJob;
+ this._status = parseInt(this.tagValue('s') as string);
+ this._title = this.tagValue('title') as string;
+ }
+
+ static from(event: NDKEvent) {
+ return new OnboardingJobEvent(event.ndk, event.rawEvent());
+ }
+
+ get jobAddress(): string {
+ return this.tagAddress();
+ }
+
+ // this.generateTags() will take care of setting d-tag
+
+ get acceptedBidAddress(): string | undefined {
+ return this.tagValue('a');
+ }
+
+ set acceptedBidAddress(bidAddress: string) {
+ // Can only have exactly one accepted bid tag
+ this.removeTag('a');
+ this.tags.push(['a', bidAddress]);
+ this.status = OnboardingJobStatus.InProgress;
+ }
+
+ get winnerFreelancer(): string | undefined {
+ return this.acceptedBidAddress?.split(':')[1];
+ }
+
+ get title(): string {
+ return this._title;
+ }
+
+ set title(title: string) {
+ this._title = title;
+ // Can only have exactly one title tag
+ this.removeTag('title');
+ this.tags.push(['title', title]);
+ }
+
+ get status(): OnboardingJobStatus {
+ return this._status;
+ }
+
+ set status(status: OnboardingJobStatus) {
+ this._status = status;
+ this.removeTag('s');
+ this.tags.push(['s', status.toString()]);
+ }
+
+ public isClosed(): boolean {
+ return this._status === OnboardingJobStatus.Resolved || this._status === OnboardingJobStatus.Failed;
+ }
+
+ get description(): string {
+ return this.content;
+ }
+
+ set description(desc: string) {
+ this.content = desc;
+ }
+
+ get tTags(): NDKTag[] {
+ return this.tags.filter((tag: NDKTag) => tag[0] === 't');
+ }
+}
diff --git a/apps/satshoot/src/lib/stores/freelance-eventstores.ts b/apps/satshoot/src/lib/stores/freelance-eventstores.ts
index 60659609..f6ae34ac 100644
--- a/apps/satshoot/src/lib/stores/freelance-eventstores.ts
+++ b/apps/satshoot/src/lib/stores/freelance-eventstores.ts
@@ -6,6 +6,7 @@ import { NDKKind } from '@nostr-dev-kit/ndk';
import { JobEvent } from '$lib/events/JobEvent';
import { BidEvent } from '$lib/events/BidEvent';
+import { OnboardingJobEvent } from '$lib/events/OnboardingJobEvent';
import { wot } from '$lib/stores/wot';
@@ -56,6 +57,11 @@ export const myOrderFilter: NDKFilter = {
kinds: [NDKKind.FreelanceOrder],
};
+// The filter's pubkey part will be filled in when user logs in
+export const myOnboardingJobFilter: NDKFilter = {
+ kinds: [NDKKind.FreelanceJob],
+};
+
export const allJobs: NDKEventStore> = get(ndk).storeSubscribe(
allJobsFilter,
subOptions,
@@ -111,3 +117,7 @@ export const myServices: NDKEventStore> = get(
export const myOrders: NDKEventStore> = get(
ndk
).storeSubscribe(myOrderFilter, subOptions, OrderEvent);
+
+export const myOnboardingJobs: NDKEventStore> = get(
+ ndk
+).storeSubscribe(myOnboardingJobFilter, subOptions, OnboardingJobEvent);
diff --git a/apps/satshoot/src/routes/[npub=user]/+page.svelte b/apps/satshoot/src/routes/[npub=user]/+page.svelte
index 741179c3..8cb4e355 100644
--- a/apps/satshoot/src/routes/[npub=user]/+page.svelte
+++ b/apps/satshoot/src/routes/[npub=user]/+page.svelte
@@ -17,6 +17,7 @@
import { debounce } from '$lib/utils/misc';
import ServicesAndBids from '$lib/components/ProfilePage/ServicesAndBids.svelte';
import OrdersAndJobs from '$lib/components/ProfilePage/OrdersAndJobs.svelte';
+ import PendingOnboardingJobs from '$lib/components/Onboarding/PendingOnboardingJobs.svelte';
import { BidEvent } from '$lib/events/BidEvent';
import { JobEvent } from '$lib/events/JobEvent';
import { ServiceEvent } from '$lib/events/ServiceEvent';
@@ -349,6 +350,7 @@
bind:this={eventContainerElement}
>
+
{#if componentOrder && componentOrder.first === 'ServicesAndBids'}