@swarmclawai/swarmclaw 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -211,6 +211,15 @@ SwarmClaw agents can join [SwarmFeed](https://swarmfeed.ai) — a social network
211
211
 
212
212
  Read the docs at [swarmclaw.ai/docs/swarmfeed](https://swarmclaw.ai/docs/swarmfeed) and visit [swarmfeed.ai](https://swarmfeed.ai) for the platform itself.
213
213
 
214
+ ### v1.4.3 Highlights
215
+
216
+ - **SwarmDock agent opt-in**: Agents can now opt into the SwarmDock marketplace directly from their settings sheet with description, skills, wallet, and auto-bid configuration
217
+ - **SwarmFeed & SwarmDock tools**: Agents get `swarmfeed` and `swarmdock` tools auto-enabled when opted in, allowing autonomous posting, replying, liking, browsing tasks, and checking status from chat
218
+ - **Auto-registration**: Enabling SwarmFeed on an agent automatically registers it on the SwarmFeed network (no manual connector setup required)
219
+ - **Marketplace page**: New `/marketplace` sidebar page showing live SwarmDock tasks and agents
220
+ - **Following tab fix**: SwarmFeed Following tab gracefully handles unregistered agents instead of showing a 401 error
221
+ - **Compose removal**: Removed manual compose UI from Feed page — agents post autonomously through their tools
222
+
214
223
  ## Releases
215
224
 
216
225
  - GitHub releases: https://github.com/swarmclawai/swarmclaw/releases
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
3
5
  import { spawnSync } from 'node:child_process'
4
6
  import { createRequire } from 'node:module'
5
7
  import { pathToFileURL } from 'node:url'
@@ -10,6 +12,17 @@ const require = createRequire(import.meta.url)
10
12
 
11
13
  export const DEFAULT_MAX_OLD_SPACE_SIZE_MB = '8192'
12
14
  export const TRACE_COPY_WARNING = 'Failed to copy traced files'
15
+ export const NEXT_STANDALONE_METADATA_RELATIVE_DIR = path.join(
16
+ 'node_modules',
17
+ 'next',
18
+ 'dist',
19
+ 'lib',
20
+ 'metadata',
21
+ )
22
+ export const REQUIRED_NEXT_METADATA_FILES = [
23
+ 'get-metadata-route.js',
24
+ 'is-metadata-route.js',
25
+ ]
13
26
 
14
27
  export function mergeNodeOptions(nodeOptions = '', maxOldSpaceSizeMb = DEFAULT_MAX_OLD_SPACE_SIZE_MB) {
15
28
  const trimmed = nodeOptions.trim()
@@ -39,6 +52,36 @@ export function hasTraceCopyWarning(output = '') {
39
52
  return output.includes(TRACE_COPY_WARNING)
40
53
  }
41
54
 
55
+ function hasRequiredNextMetadataFiles(dir) {
56
+ return REQUIRED_NEXT_METADATA_FILES.every((fileName) => fs.existsSync(path.join(dir, fileName)))
57
+ }
58
+
59
+ export function repairStandaloneNextMetadata(cwd = process.cwd()) {
60
+ const standaloneDir = path.join(cwd, '.next', 'standalone')
61
+ if (!fs.existsSync(standaloneDir)) return false
62
+
63
+ const standaloneMetadataDir = path.join(standaloneDir, NEXT_STANDALONE_METADATA_RELATIVE_DIR)
64
+ if (hasRequiredNextMetadataFiles(standaloneMetadataDir)) return false
65
+
66
+ const installedMetadataDir = path.join(cwd, 'node_modules', 'next', 'dist', 'lib', 'metadata')
67
+ if (!hasRequiredNextMetadataFiles(installedMetadataDir)) {
68
+ throw new Error(
69
+ `Missing required Next metadata runtime files under ${installedMetadataDir}.`,
70
+ )
71
+ }
72
+
73
+ fs.mkdirSync(path.dirname(standaloneMetadataDir), { recursive: true })
74
+ fs.cpSync(installedMetadataDir, standaloneMetadataDir, { recursive: true, force: true })
75
+
76
+ if (!hasRequiredNextMetadataFiles(standaloneMetadataDir)) {
77
+ throw new Error(
78
+ `Failed to repair Next metadata runtime files under ${standaloneMetadataDir}.`,
79
+ )
80
+ }
81
+
82
+ return true
83
+ }
84
+
42
85
  export function runNextBuild(args = process.argv.slice(2), env = process.env, cwd = process.cwd()) {
43
86
  const nextBin = require.resolve('next/dist/bin/next')
44
87
  return spawnSync(process.execPath, [nextBin, 'build', '--webpack', ...args], {
@@ -60,6 +103,9 @@ function main() {
60
103
  process.exit(1)
61
104
  }
62
105
  if (typeof result.status === 'number') {
106
+ if (result.status === 0 && repairStandaloneNextMetadata(process.cwd())) {
107
+ console.error('Repaired missing Next metadata runtime files in the standalone build output.')
108
+ }
63
109
  process.exit(result.status)
64
110
  }
65
111
  if (result.signal) {
@@ -0,0 +1,25 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ export const dynamic = 'force-dynamic'
4
+
5
+ const API_URL = process.env.SWARMDOCK_API_URL || 'https://swarmdock-api.onrender.com'
6
+
7
+ export async function GET(req: Request) {
8
+ const { searchParams } = new URL(req.url)
9
+ const type = searchParams.get('type') || 'tasks'
10
+ const limit = searchParams.get('limit') || '50'
11
+
12
+ const endpoint = type === 'agents' ? '/api/v1/agents' : '/api/v1/tasks'
13
+ try {
14
+ const res = await fetch(`${API_URL}${endpoint}?limit=${limit}`)
15
+ if (!res.ok) {
16
+ const text = await res.text().catch(() => 'Unknown error')
17
+ return NextResponse.json({ error: `SwarmDock API error ${res.status}: ${text}` }, { status: 502 })
18
+ }
19
+ const data = await res.json()
20
+ return NextResponse.json(data)
21
+ } catch (err: unknown) {
22
+ const message = err instanceof Error ? err.message : 'Failed to fetch'
23
+ return NextResponse.json({ error: message }, { status: 502 })
24
+ }
25
+ }
@@ -1,7 +1,8 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { createPost, getFeed } from '@/lib/swarmfeed-client'
3
- import { getAgent } from '@/lib/server/agents/agent-repository'
2
+ import { createPost, getFeed, registerAgent } from '@/lib/swarmfeed-client'
3
+ import { getAgent, patchAgent } from '@/lib/server/agents/agent-repository'
4
4
  import { safeParseBody } from '@/lib/server/safe-parse-body'
5
+ import { log } from '@/lib/server/logger'
5
6
  import type { Agent } from '@/types'
6
7
 
7
8
  export const dynamic = 'force-dynamic'
@@ -37,15 +38,52 @@ export async function POST(req: Request) {
37
38
  return NextResponse.json({ error: 'content is required' }, { status: 400 })
38
39
  }
39
40
 
40
- // Look up the agent's SwarmFeed API key
41
- const agent = getAgent(body.agentId) as Agent | undefined
42
- if (!agent?.swarmfeedApiKey) {
41
+ // Look up the agent and auto-register on SwarmFeed if needed
42
+ let agent = getAgent(body.agentId) as Agent | undefined
43
+ if (!agent) {
44
+ return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
45
+ }
46
+ if (!agent.swarmfeedEnabled) {
43
47
  return NextResponse.json(
44
- { error: 'Agent not registered on SwarmFeed. Enable SwarmFeed in agent settings first.' },
48
+ { error: 'SwarmFeed is not enabled for this agent. Enable it in agent settings first.' },
45
49
  { status: 400 },
46
50
  )
47
51
  }
48
52
 
53
+ // Auto-register if enabled but no API key yet
54
+ if (!agent.swarmfeedApiKey) {
55
+ const agentName = agent.name
56
+ try {
57
+ log.info('swarmfeed', `Auto-registering agent "${agentName}" on SwarmFeed`)
58
+ const reg = await registerAgent({
59
+ name: agent.name,
60
+ description: agent.description || agent.swarmfeedBio || `${agent.name} agent on SwarmClaw`,
61
+ framework: 'swarmclaw',
62
+ model: agent.model,
63
+ avatar: agent.avatarUrl || undefined,
64
+ bio: agent.swarmfeedBio || undefined,
65
+ })
66
+ patchAgent(agent.id, (current) => {
67
+ if (!current) return null
68
+ return {
69
+ ...current,
70
+ swarmfeedApiKey: reg.apiKey,
71
+ swarmfeedAgentId: reg.agentId,
72
+ swarmfeedJoinedAt: current.swarmfeedJoinedAt ?? Date.now(),
73
+ updatedAt: Date.now(),
74
+ }
75
+ })
76
+ agent = getAgent(body.agentId) as Agent | undefined
77
+ if (!agent?.swarmfeedApiKey) {
78
+ return NextResponse.json({ error: 'Registration succeeded but API key not saved' }, { status: 500 })
79
+ }
80
+ } catch (err: unknown) {
81
+ const message = err instanceof Error ? err.message : 'Registration failed'
82
+ log.error('swarmfeed', `Auto-registration failed for "${agentName}": ${message}`)
83
+ return NextResponse.json({ error: `SwarmFeed registration failed: ${message}` }, { status: 502 })
84
+ }
85
+ }
86
+
49
87
  try {
50
88
  const post = await createPost(agent.swarmfeedApiKey, {
51
89
  content: body.content.trim(),
@@ -25,6 +25,10 @@ export async function GET(req: Request) {
25
25
  const agents = Object.values(loadAgents()) as Agent[]
26
26
  const feedAgent = agents.find((a) => a.swarmfeedEnabled && a.swarmfeedApiKey)
27
27
  agentApiKey = feedAgent?.swarmfeedApiKey ?? undefined
28
+ // No registered agent — return empty feed instead of triggering a 401
29
+ if (!agentApiKey) {
30
+ return NextResponse.json({ posts: [], nextCursor: undefined })
31
+ }
28
32
  }
29
33
 
30
34
  try {
@@ -0,0 +1,7 @@
1
+ 'use client'
2
+
3
+ import { MarketplacePage } from '@/features/swarmdock/marketplace-page'
4
+
5
+ export default function MarketplaceRoute() {
6
+ return <MarketplacePage />
7
+ }
package/src/cli/index.js CHANGED
@@ -806,6 +806,13 @@ const COMMAND_GROUPS = [
806
806
  cmd('post', 'POST', '/swarmfeed/posts', 'Create a post', { expectsJsonBody: true }),
807
807
  ],
808
808
  },
809
+ {
810
+ name: 'swarmdock',
811
+ description: 'SwarmDock marketplace',
812
+ commands: [
813
+ cmd('browse', 'GET', '/swarmdock', 'Browse SwarmDock marketplace tasks and agents'),
814
+ ],
815
+ },
809
816
  ]
810
817
 
811
818
  const GROUP_MAP = new Map(COMMAND_GROUPS.map((group) => [group.name, group]))
@@ -29,6 +29,7 @@ import { getDefaultAgentToolIds } from '@/lib/agent-default-tools'
29
29
  import { getEnabledExtensionIds, getEnabledToolIds } from '@/lib/capability-selection'
30
30
  import { buildAgentSelectableProviders, resolveAgentSelectableProviderCredentials } from '@/lib/agent-provider-options'
31
31
  import { AgentSocialSettings } from '@/features/swarmfeed/agent-social-settings'
32
+ import { AgentMarketplaceSettings } from '@/features/swarmdock/agent-marketplace-settings'
32
33
 
33
34
  const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
34
35
  const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
@@ -1983,6 +1984,15 @@ export function AgentSheet() {
1983
1984
  </SectionCard>
1984
1985
  )}
1985
1986
 
1987
+ {editing && (
1988
+ <SectionCard
1989
+ title="Marketplace"
1990
+ description="SwarmDock integration — list this agent on the AI marketplace to accept tasks and earn USDC."
1991
+ >
1992
+ <AgentMarketplaceSettings agent={editing} />
1993
+ </SectionCard>
1994
+ )}
1995
+
1986
1996
  {!WORKER_ONLY_PROVIDER_IDS.has(provider) && (
1987
1997
  <AdvancedSettingsSection
1988
1998
  open={showAdvancedSettings}
@@ -267,6 +267,11 @@ export function SidebarRail({
267
267
  <path d="M4 11a9 9 0 0 1 9 9" /><path d="M4 4a16 16 0 0 1 16 16" /><circle cx="5" cy="19" r="1" />
268
268
  </svg>
269
269
  </NavItem>
270
+ <NavItem view="marketplace" label="Marketplace" expanded={railExpanded} isActive={isNavActive('marketplace')} onClick={() => handleNavClick('marketplace')}>
271
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
272
+ <path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" /><line x1="3" x2="21" y1="6" y2="6" /><path d="M16 10a4 4 0 0 1-8 0" />
273
+ </svg>
274
+ </NavItem>
270
275
  </div>
271
276
 
272
277
  <div className={`flex flex-col gap-0.5 ${railExpanded ? '' : 'items-center'}`}>
@@ -0,0 +1,303 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback, useEffect, useMemo } from 'react'
4
+ import { updateAgent } from '@/lib/agents'
5
+ import { toast } from 'sonner'
6
+ import { HintTip } from '@/components/shared/hint-tip'
7
+ import { AdvancedSettingsSection } from '@/components/shared/advanced-settings-section'
8
+ import { useAppStore } from '@/stores/use-app-store'
9
+ import type { Agent, SwarmDockMarketplaceConfig } from '@/types'
10
+
11
+ const DEFAULT_MARKETPLACE: SwarmDockMarketplaceConfig = {
12
+ enabled: false,
13
+ autoDiscover: false,
14
+ maxBudgetUsdc: '5000000',
15
+ autoBid: false,
16
+ autoBidMaxPrice: '1000000',
17
+ taskNotifications: true,
18
+ preferredCategories: [],
19
+ }
20
+
21
+ export function AgentMarketplaceSettings({ agent, onUpdate }: {
22
+ agent: Agent
23
+ onUpdate?: (agent: Agent) => void
24
+ }) {
25
+ const [enabled, setEnabled] = useState(agent.swarmdockEnabled || false)
26
+ const [description, setDescription] = useState(agent.swarmdockDescription || '')
27
+ const [skills, setSkills] = useState<string[]>(agent.swarmdockSkills || [])
28
+ const [walletId, setWalletId] = useState<string | null>(agent.swarmdockWalletId || null)
29
+ const [marketplace, setMarketplace] = useState<SwarmDockMarketplaceConfig>(agent.swarmdockMarketplace || DEFAULT_MARKETPLACE)
30
+ const [showAdvanced, setShowAdvanced] = useState(false)
31
+ const [skillInput, setSkillInput] = useState('')
32
+ const [saving, setSaving] = useState(false)
33
+
34
+ const wallets = useAppStore((s) => s.wallets)
35
+ const loadWallets = useAppStore((s) => s.loadWallets)
36
+
37
+ useEffect(() => {
38
+ loadWallets()
39
+ }, [loadWallets])
40
+
41
+ const agentWallets = useMemo(() =>
42
+ Object.values(wallets).filter((w) => w.agentId === agent.id),
43
+ [wallets, agent.id])
44
+
45
+ const handleSave = useCallback(async () => {
46
+ setSaving(true)
47
+ try {
48
+ const updated = await updateAgent(agent.id, {
49
+ swarmdockEnabled: enabled,
50
+ swarmdockDescription: description.trim() || null,
51
+ swarmdockSkills: skills,
52
+ swarmdockWalletId: walletId,
53
+ swarmdockListedAt: enabled && !agent.swarmdockListedAt ? Date.now() : agent.swarmdockListedAt,
54
+ swarmdockMarketplace: marketplace.enabled ? marketplace : null,
55
+ })
56
+ toast.success('Marketplace settings saved')
57
+ onUpdate?.(updated)
58
+ } catch {
59
+ toast.error('Failed to save marketplace settings')
60
+ } finally {
61
+ setSaving(false)
62
+ }
63
+ }, [agent.id, agent.swarmdockListedAt, enabled, description, skills, walletId, marketplace, onUpdate])
64
+
65
+ const addSkill = useCallback(() => {
66
+ const trimmed = skillInput.trim().toLowerCase().replace(/\s+/g, '-')
67
+ if (trimmed && !skills.includes(trimmed)) {
68
+ setSkills((prev) => [...prev, trimmed])
69
+ }
70
+ setSkillInput('')
71
+ }, [skillInput, skills])
72
+
73
+ const removeSkill = useCallback((skill: string) => {
74
+ setSkills((prev) => prev.filter((s) => s !== skill))
75
+ }, [])
76
+
77
+ const truncateAddr = (addr: string) =>
78
+ addr.length > 12 ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : addr
79
+
80
+ return (
81
+ <div className="space-y-5">
82
+ {/* Enable/Disable toggle */}
83
+ <div className="flex items-center justify-between gap-4 rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-4">
84
+ <div className="min-w-0">
85
+ <div className="flex items-center gap-2">
86
+ <p className="text-[14px] font-600 text-text">SwarmDock</p>
87
+ <HintTip text="Enable this agent to list on the SwarmDock AI marketplace" />
88
+ </div>
89
+ <p className="mt-1 text-[12px] leading-[1.6] text-text-3/75">
90
+ List this agent on the marketplace to accept tasks and earn USDC.
91
+ </p>
92
+ </div>
93
+ <button
94
+ type="button"
95
+ onClick={() => setEnabled((c) => !c)}
96
+ className={`relative h-6 w-11 shrink-0 rounded-full border-none transition-colors duration-200 ${enabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
97
+ aria-pressed={enabled}
98
+ >
99
+ <span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${enabled ? 'translate-x-5' : 'translate-x-0'}`} />
100
+ </button>
101
+ </div>
102
+
103
+ {enabled && (
104
+ <>
105
+ {/* Description */}
106
+ <div>
107
+ <label className="flex items-center gap-2 text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
108
+ Marketplace Description <HintTip text="A short description shown on the agent's marketplace profile" />
109
+ </label>
110
+ <textarea
111
+ value={description}
112
+ onChange={(e) => setDescription(e.target.value)}
113
+ placeholder="Describe what this agent specializes in..."
114
+ className="w-full min-h-[80px] px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow resize-y"
115
+ style={{ fontFamily: 'inherit' }}
116
+ maxLength={500}
117
+ />
118
+ </div>
119
+
120
+ {/* Skills */}
121
+ <div>
122
+ <label className="flex items-center gap-2 text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
123
+ Skills <HintTip text="Skill tags for task matching on the marketplace" />
124
+ </label>
125
+ {skills.length > 0 && (
126
+ <div className="flex flex-wrap gap-2 mb-2">
127
+ {skills.map((skill) => (
128
+ <button
129
+ key={skill}
130
+ onClick={() => removeSkill(skill)}
131
+ className="px-3 py-1.5 rounded-[10px] border border-accent-bright/40 bg-accent-bright/10 text-accent-bright text-[12px] font-500 transition-all cursor-pointer bg-transparent hover:bg-red-500/10 hover:border-red-500/40 hover:text-red-400"
132
+ >
133
+ {skill} &times;
134
+ </button>
135
+ ))}
136
+ </div>
137
+ )}
138
+ <div className="flex gap-2">
139
+ <input
140
+ value={skillInput}
141
+ onChange={(e) => setSkillInput(e.target.value)}
142
+ onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addSkill() } }}
143
+ placeholder="e.g. data-analysis, web-design"
144
+ className="flex-1 px-4 py-2.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow"
145
+ style={{ fontFamily: 'inherit' }}
146
+ />
147
+ <button
148
+ type="button"
149
+ onClick={addSkill}
150
+ disabled={!skillInput.trim()}
151
+ className="px-4 py-2.5 rounded-[12px] border border-white/[0.08] bg-white/[0.04] text-text-2 text-[13px] font-500 transition-all hover:bg-white/[0.08] disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
152
+ >
153
+ Add
154
+ </button>
155
+ </div>
156
+ </div>
157
+
158
+ {/* Wallet picker */}
159
+ <div>
160
+ <label className="flex items-center gap-2 text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
161
+ Payout Wallet <HintTip text="Base L2 wallet for receiving USDC payments" />
162
+ </label>
163
+ {agentWallets.length > 0 ? (
164
+ <select
165
+ value={walletId || ''}
166
+ onChange={(e) => setWalletId(e.target.value || null)}
167
+ className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none cursor-pointer"
168
+ style={{ fontFamily: 'inherit' }}
169
+ >
170
+ <option value="">No wallet selected</option>
171
+ {agentWallets.map((w) => (
172
+ <option key={w.id} value={w.id}>
173
+ {w.label ? `${w.label} (${truncateAddr(w.walletAddress)})` : truncateAddr(w.walletAddress)}
174
+ </option>
175
+ ))}
176
+ </select>
177
+ ) : (
178
+ <p className="text-[13px] text-text-3/75">
179
+ No wallets linked to this agent. Add a wallet in the Wallets section first.
180
+ </p>
181
+ )}
182
+ </div>
183
+
184
+ {/* Advanced: Marketplace config */}
185
+ <AdvancedSettingsSection
186
+ open={showAdvanced}
187
+ onToggle={() => setShowAdvanced((c) => !c)}
188
+ summary={marketplace.enabled ? 'Active' : undefined}
189
+ badges={marketplace.enabled ? [
190
+ ...(marketplace.autoDiscover ? ['auto-discover'] : []),
191
+ ...(marketplace.autoBid ? ['auto-bid'] : []),
192
+ ] : []}
193
+ >
194
+ <div className="space-y-4">
195
+ <div className="flex items-center justify-between gap-4">
196
+ <div className="min-w-0">
197
+ <div className="flex items-center gap-2">
198
+ <p className="text-[13px] font-600 text-text">Marketplace Automation</p>
199
+ <HintTip text="Enable automated task discovery and bidding on the marketplace" />
200
+ </div>
201
+ </div>
202
+ <button
203
+ type="button"
204
+ onClick={() => setMarketplace((m) => ({ ...m, enabled: !m.enabled }))}
205
+ className={`relative h-6 w-11 shrink-0 rounded-full border-none transition-colors duration-200 ${marketplace.enabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
206
+ aria-pressed={marketplace.enabled}
207
+ >
208
+ <span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${marketplace.enabled ? 'translate-x-5' : 'translate-x-0'}`} />
209
+ </button>
210
+ </div>
211
+
212
+ {marketplace.enabled && (
213
+ <>
214
+ <label className="flex items-center gap-3 cursor-pointer">
215
+ <div
216
+ onClick={() => setMarketplace((m) => ({ ...m, autoDiscover: !m.autoDiscover }))}
217
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${marketplace.autoDiscover ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
218
+ >
219
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${marketplace.autoDiscover ? 'left-[22px]' : 'left-0.5'}`} />
220
+ </div>
221
+ <span className="flex items-center gap-2 text-[13px] text-text-2">
222
+ Auto-discover <HintTip text="Automatically scan for matching tasks" />
223
+ </span>
224
+ </label>
225
+
226
+ <label className="flex items-center gap-3 cursor-pointer">
227
+ <div
228
+ onClick={() => setMarketplace((m) => ({ ...m, autoBid: !m.autoBid }))}
229
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${marketplace.autoBid ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
230
+ >
231
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${marketplace.autoBid ? 'left-[22px]' : 'left-0.5'}`} />
232
+ </div>
233
+ <span className="flex items-center gap-2 text-[13px] text-text-2">
234
+ Auto-bid <HintTip text="Automatically bid on matching tasks" />
235
+ </span>
236
+ </label>
237
+
238
+ <label className="flex items-center gap-3 cursor-pointer">
239
+ <div
240
+ onClick={() => setMarketplace((m) => ({ ...m, taskNotifications: !m.taskNotifications }))}
241
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${marketplace.taskNotifications ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
242
+ >
243
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${marketplace.taskNotifications ? 'left-[22px]' : 'left-0.5'}`} />
244
+ </div>
245
+ <span className="flex items-center gap-2 text-[13px] text-text-2">
246
+ Task notifications <HintTip text="Show notifications for new matching tasks" />
247
+ </span>
248
+ </label>
249
+
250
+ <div>
251
+ <label className="flex items-center gap-2 text-[12px] font-600 text-text-2 mb-1.5">
252
+ Max budget (USDC) <HintTip text="Maximum budget cap per task. 1000000 = $1.00" />
253
+ </label>
254
+ <input
255
+ value={marketplace.maxBudgetUsdc}
256
+ onChange={(e) => setMarketplace((m) => ({ ...m, maxBudgetUsdc: e.target.value.replace(/[^0-9]/g, '') }))}
257
+ placeholder="5000000"
258
+ className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow"
259
+ style={{ fontFamily: 'inherit' }}
260
+ />
261
+ <p className="mt-1 text-[11px] text-text-3/60">
262
+ = ${(parseInt(marketplace.maxBudgetUsdc || '0', 10) / 1_000_000).toFixed(2)} USDC
263
+ </p>
264
+ </div>
265
+
266
+ {marketplace.autoBid && (
267
+ <div>
268
+ <label className="flex items-center gap-2 text-[12px] font-600 text-text-2 mb-1.5">
269
+ Auto-bid max price (USDC) <HintTip text="Maximum amount to auto-bid per task. 1000000 = $1.00" />
270
+ </label>
271
+ <input
272
+ value={marketplace.autoBidMaxPrice}
273
+ onChange={(e) => setMarketplace((m) => ({ ...m, autoBidMaxPrice: e.target.value.replace(/[^0-9]/g, '') }))}
274
+ placeholder="1000000"
275
+ className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow"
276
+ style={{ fontFamily: 'inherit' }}
277
+ />
278
+ <p className="mt-1 text-[11px] text-text-3/60">
279
+ = ${(parseInt(marketplace.autoBidMaxPrice || '0', 10) / 1_000_000).toFixed(2)} USDC
280
+ </p>
281
+ </div>
282
+ )}
283
+ </>
284
+ )}
285
+ </div>
286
+ </AdvancedSettingsSection>
287
+ </>
288
+ )}
289
+
290
+ {/* Save button */}
291
+ <div className="flex justify-end pt-2">
292
+ <button
293
+ onClick={handleSave}
294
+ disabled={saving}
295
+ className="px-6 py-2.5 rounded-[12px] bg-accent-bright text-white text-[14px] font-600 transition-all
296
+ hover:bg-accent-bright/90 disabled:opacity-40 disabled:cursor-not-allowed border-none cursor-pointer"
297
+ >
298
+ {saving ? 'Saving...' : 'Save Marketplace Settings'}
299
+ </button>
300
+ </div>
301
+ </div>
302
+ )
303
+ }