diff --git a/agent/agent.js b/agent/agent.js index bc74ac51..29c13022 100644 --- a/agent/agent.js +++ b/agent/agent.js @@ -10,6 +10,7 @@ import { createEd25519Signer } from '@x402/stellar'; import { ExactStellarScheme } from '@x402/stellar/exact/client'; import { buildRunSummary, writeRunSummary } from './runSummary.js'; import { stroopsToUsdcDisplay } from '../packages/stroops/index.js'; +import { createClient } from '../packages/client/index.js'; // ── Config ──────────────────────────────────────────────────────────────────── @@ -120,81 +121,77 @@ export const EVENT = { // ── Credit scoring helpers ──────────────────────────────────────────────────── +const apiClient = createClient({ + baseUrl: LODESTAR_API_URL, + timeoutMs: FETCH_TIMEOUT_MS, +}); + let currentScore = null; export async function ensureRegistered() { try { - const res = await fetchWithTimeout(`${LODESTAR_API_URL}/api/agents/${AGENT_ADDRESS}`); - if (res.status === 503) { + const data = await apiClient.getAgent(AGENT_ADDRESS); + const agent = data.agent ?? data; + currentScore = agent.score; + const policy = data.policy; + const dailyLimitUsdc = policy + ? stroopsToUsdcDisplay(policy.max_per_day_stroops) + : null; + logger.info( + { event: EVENT.AGENT_REGISTERED, agentAddress: AGENT_ADDRESS, score: agent.score, dailyLimitUsdc, scoringEnabled: true }, + 'Already registered' + ); + return true; + } catch (err) { + if (err.status === 503) { logger.info( { event: EVENT.AGENT_REGISTERED, agentAddress: AGENT_ADDRESS, scoringEnabled: false }, 'Agents contract not deployed — scoring disabled' ); return false; } - if (res.ok) { - const data = await res.json(); - const agent = data.agent ?? data; - currentScore = agent.score; - const policy = data.policy; - const dailyLimitUsdc = policy - ? stroopsToUsdcDisplay(policy.max_per_day_stroops) - : null; - logger.info( - { event: EVENT.AGENT_REGISTERED, agentAddress: AGENT_ADDRESS, score: agent.score, dailyLimitUsdc, scoringEnabled: true }, - 'Already registered' - ); - return true; - } - if (res.status === 404) { + if (err.status === 404) { logger.info( { event: EVENT.AGENT_REGISTERED, agentAddress: AGENT_ADDRESS }, 'Not registered — registering now…' ); - const regRes = await fetchWithTimeout(`${LODESTAR_API_URL}/api/agents/register`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ + try { + await apiClient.registerAgent({ agentAddress: AGENT_ADDRESS, name: AGENT_NAME, description: AGENT_DESC, maxPerTxUsdc: MAX_PER_TX, maxPerDayUsdc: MAX_PER_DAY, allowedCategories: ALLOWED_CATS, - }), - }); - if (regRes.ok) { + }); currentScore = 100; logger.info( { event: EVENT.AGENT_REGISTERED, agentAddress: AGENT_ADDRESS, score: 100, scoringEnabled: true }, 'Registered — starting score: 100' ); return true; + } catch (regErr) { + logger.warn( + { event: EVENT.AGENT_REGISTERED, agentAddress: AGENT_ADDRESS, scoringEnabled: false, err: regErr.body || regErr.message }, + 'Registration failed — scoring disabled' + ); + return false; } - const err = await regRes.json().catch(() => ({})); - logger.warn( - { event: EVENT.AGENT_REGISTERED, agentAddress: AGENT_ADDRESS, scoringEnabled: false, err }, - 'Registration failed — scoring disabled' - ); - return false; } - } catch (err) { logger.warn( { event: EVENT.AGENT_REGISTERED, agentAddress: AGENT_ADDRESS, scoringEnabled: false, err }, 'Could not reach agents API — scoring disabled' ); + return false; } - return false; } async function checkSpend(amountUsdc, category) { try { - const res = await fetchWithTimeout( - `${LODESTAR_API_URL}/api/agents/${AGENT_ADDRESS}/can-spend` + - `?amount=${encodeURIComponent(amountUsdc)}&category=${encodeURIComponent(category)}` - ); - if (!res.ok) return { allowed: true, reason: 'OK' }; - return await res.json(); + return await apiClient.checkAgentCanSpend(AGENT_ADDRESS, { + amount: amountUsdc, + category, + }); } catch { return { allowed: true, reason: 'OK' }; } @@ -202,21 +199,20 @@ async function checkSpend(amountUsdc, category) { async function recordOutcome(amountUsdc, success, serviceId) { try { - const body = JSON.stringify({ amountUsdc, success, serviceId }); - const headers = { 'Content-Type': 'application/json' }; + const headers = {}; if (LODESTAR_HMAC_SECRET) { + const body = JSON.stringify({ amountUsdc, success, serviceId }); headers['X-Lodestar-Signature'] = crypto .createHmac('sha256', LODESTAR_HMAC_SECRET) .update(body) .digest('hex'); } - const res = await fetchWithTimeout(`${LODESTAR_API_URL}/api/agents/${AGENT_ADDRESS}/payment`, { - method: 'POST', - headers, - body, - }); - if (res.ok) { - const data = await res.json(); + const data = await apiClient.recordAgentPayment( + AGENT_ADDRESS, + { amountUsdc, success, serviceId }, + { headers } + ); + if (data && data.newScore !== undefined) { const scoreBefore = currentScore; currentScore = data.newScore; logger.info( @@ -274,22 +270,13 @@ function buildHttpClient() { // ── Registry helpers ────────────────────────────────────────────────────────── async function fetchServices(category) { - const res = await fetchWithTimeout(`${LODESTAR_API_URL}/api/services?category=${category}`); - if (!res.ok) throw new Error(`Registry fetch failed: ${res.status}`); - const body = await res.json(); - return body.services ?? []; + const data = await apiClient.getServices({ category }); + return data.services ?? []; } async function submitReputation(id, positive) { try { - const res = await fetchWithTimeout(`${LODESTAR_API_URL}/api/reputation/${id}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ positive, agent: AGENT_ADDRESS }), - }); - if (!res.ok) { - logger.debug({ status: res.status }, 'Reputation vote not applied (best-effort)'); - } + await apiClient.submitReputation(id, { positive, agent: AGENT_ADDRESS }); } catch { // Intentionally best-effort — a failed vote must not abort the agent run. } diff --git a/agent/package.json b/agent/package.json index 20787af1..861deda8 100644 --- a/agent/package.json +++ b/agent/package.json @@ -10,6 +10,7 @@ "test": "vitest run" }, "dependencies": { + "@lodestar/client": "file:../packages/client", "@stellar/stellar-sdk": "^13.0.0", "@x402/core": "^2.9.0", "@x402/stellar": "^2.9.0", diff --git a/frontend/app/agents/[address]/page.tsx b/frontend/app/agents/[address]/page.tsx index 11799e41..725b7a3a 100644 --- a/frontend/app/agents/[address]/page.tsx +++ b/frontend/app/agents/[address]/page.tsx @@ -356,7 +356,7 @@ export default function AgentProfilePage() { - + ); diff --git a/frontend/app/globals.css b/frontend/app/globals.css index b860e912..e10498c9 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -66,7 +66,7 @@ body { } .input { - @apply bg-background border border-border rounded-lg px-4 py-2.5 text-sm text-primary placeholder:text-secondary focus:outline-none focus:ring-1 focus:ring-primary/30 transition-shadow; + @apply bg-background border border-border rounded-lg px-4 py-2.5 text-sm text-primary placeholder:text-secondary focus:outline-none focus:ring-1 focus:ring-primary transition-shadow; } } diff --git a/frontend/components/CreditScoreDemo.tsx b/frontend/components/CreditScoreDemo.tsx index bcd03276..831da8d4 100644 --- a/frontend/components/CreditScoreDemo.tsx +++ b/frontend/components/CreditScoreDemo.tsx @@ -77,7 +77,7 @@ export default function CreditScoreDemo() { setSpendResult(res.allowed ? 'allowed' : 'blocked'); setSpendDetail(res.allowed ? `$${SIMULATE_AMOUNT} USDC within daily limit of $${DAILY_LIMIT_USDC} USDC` - : res.reason); + : res.reason ?? 'Policy limit exceeded'); } catch { setSpendResult('blocked'); setSpendDetail('Policy check failed'); diff --git a/frontend/components/RegisterForm.tsx b/frontend/components/RegisterForm.tsx index 9c960ab9..5304d425 100644 --- a/frontend/components/RegisterForm.tsx +++ b/frontend/components/RegisterForm.tsx @@ -62,16 +62,17 @@ function validate(f: FormState): Record { export default function RegisterForm({ walletAddress }: Props) { const [form, setForm] = useState(EMPTY); - const [errors, setErrors] = useState>({}); + const [errors, setErrors] = useState>(() => validate(EMPTY)); + const [touched, setTouched] = useState>({}); const [submitting, setSubmitting] = useState(false); const [pendingTx, setPendingTx] = useState<{ txHash: string } | null>(null); const [result, setResult] = useState<{ txHash: string; id: number } | null>(null); const [submitError, setSubmitError] = useState(''); function set(field: keyof FormState, value: string) { - setForm((prev) => ({ ...prev, [field]: value })); - // Validate on change const updatedForm = { ...form, [field]: value }; + setForm(updatedForm); + setTouched((prev) => ({ ...prev, [field]: true })); const errs = validate(updatedForm); setErrors(errs); } @@ -81,6 +82,7 @@ export default function RegisterForm({ walletAddress }: Props) { const errs = validate(form); if (Object.keys(errs).length > 0) { setErrors(errs); + setTouched({ name: true, description: true, endpoint: true, price_usdc: true, category: true }); return; } setSubmitting(true); @@ -93,6 +95,8 @@ export default function RegisterForm({ walletAddress }: Props) { await new Promise(resolve => setTimeout(resolve, 2000)); setResult(res); setForm(EMPTY); + setErrors(validate(EMPTY)); + setTouched({}); } catch (err) { setSubmitError(err instanceof Error ? err.message : 'Registration failed'); } finally { @@ -152,66 +156,73 @@ export default function RegisterForm({ walletAddress }: Props) { return (
set('name', e.target.value)} placeholder="My Weather API" disabled={submitting} - className={input(!!errors.name)} + className={input(!!(touched.name && errors.name))} />