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
82 changes: 77 additions & 5 deletions apps/logicsrc-web/contract/logicsrc-web.contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,18 +173,20 @@ describe("POST /api/hire-us/coinpay-checkout", () => {
expect(createCall?.[1]?.headers).toMatchObject({ authorization: "Bearer cp_test_key" });
expect(createBody).toMatchObject({
business_id: "business-123",
amount_usd: 250,
amount_usd: 4000,
payment_method: "both",
currency: "usdc_pol",
blockchain: "USDC_POL",
description: "LogicSRC Hire Us - $250/week",
description: "LogicSRC Hire Us - 10h @ $400/hour",
success_url: "https://logicsrc.test/hire-us?payment=success",
cancel_url: "https://logicsrc.test/hire-us?payment=cancelled",
redirect_url: "https://logicsrc.test/hire-us?payment=coinpay",
webhook_url: "https://logicsrc.test/api/webhooks/coinpay",
metadata: {
product: "logicsrc-hire-us",
interval: "week",
billing: "metered_hours",
hours: 10,
rate_usd_per_hour: 400,
source: "logicsrc.com/hire-us",
buyer_email: "buyer@example.com"
}
Expand Down Expand Up @@ -241,7 +243,71 @@ describe("POST /api/hire-us/coinpay-checkout", () => {
const body = await response.json();

expect(response.status).toBe(201);
expect(body.payment.amount_usd).toBe(250);
expect(body.payment.amount_usd).toBe(4000);
});

it("prices the checkout from the approved hours", async () => {
process.env.COINPAY_API_KEY = "cp_test_key";
process.env.COINPAY_API_URL = "https://coinpayportal.example";
process.env.COINPAY_BUSINESS_ID = "business-123";
process.env.COINPAY_ELIGIBILITY_MERCHANT_ID = "merchant-123";
process.env.COINPAY_HIRE_US_BLOCKCHAIN = "USDC_POL";

const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
const url = typeof input === "string" ? input : input.toString();
if (url.includes("/api/payments/merchant-eligibility")) {
return jsonResponse({ success: true, accepts_card: true, accepts_crypto: true, chains: ["USDC_POL"] });
}
return jsonResponse({ success: true, payment: { id: "pay_123", amount_usd: 10100 } }, 201);
});

const response = await coinpayCheckout(
new NextRequest("http://localhost/api/hire-us/coinpay-checkout", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ hours: 25.25 })
})
);
const body = await response.json();

const createCall = fetchMock.mock.calls.find(([input]) =>
(typeof input === "string" ? input : input.toString()).includes("/api/payments/create")
);
const createBody = JSON.parse((createCall?.[1]?.body as string) ?? "{}");

expect(response.status).toBe(201);
expect(createBody.amount_usd).toBe(10100);
expect(createBody.description).toBe("LogicSRC Hire Us - 25.25h @ $400/hour");
expect(createBody.metadata).toMatchObject({ billing: "metered_hours", hours: 25.25, rate_usd_per_hour: 400 });
expect(body.payment).toMatchObject({ amount_usd: 10100, hours: 25.25, rate_usd_per_hour: 400 });
});

it("rejects hours below the minimum engagement or off the quarter-hour", async () => {
process.env.COINPAY_API_KEY = "cp_test_key";
process.env.COINPAY_API_URL = "https://coinpayportal.example";
process.env.COINPAY_BUSINESS_ID = "business-123";
process.env.COINPAY_ELIGIBILITY_MERCHANT_ID = "merchant-123";

const fetchMock = vi.spyOn(globalThis, "fetch");

for (const hours of [9.75, 12.3, "many", -40]) {
const response = await coinpayCheckout(
new NextRequest("http://localhost/api/hire-us/coinpay-checkout", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ hours })
})
);
const body = await response.json();

expect(response.status).toBe(422);
expect(body).toEqual({
success: false,
error: "Approved hours must be a quarter-hour increment of at least 10"
});
}

expect(fetchMock).not.toHaveBeenCalled();
});

it("does not create checkout when no payment rail is available", async () => {
Expand Down Expand Up @@ -290,7 +356,13 @@ describe("POST /api/hire-us/project-request", () => {
expect(response.status).toBe(202);
expect(body).toMatchObject({
success: true,
request: { status: "pending_acceptance", amount_usd: 250, interval: "week", invoice: "created_after_acceptance" }
request: {
status: "pending_acceptance",
rate_usd_per_hour: 400,
billing: "metered_hours",
minimum_hours: 10,
invoice: "created_after_acceptance"
}
});
expect(body.request.id).toMatch(/^hire_/);
});
Expand Down
7 changes: 5 additions & 2 deletions apps/logicsrc-web/e2e/logicsrc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,17 @@ test.describe("LogicSRC PWA", () => {
await page.goto("/hire-us");

await expect(page.getByRole("heading", { name: "Hire Us", exact: true })).toBeVisible();
await expect(page.locator(".price-row strong", { hasText: "$250" })).toBeVisible();
await expect(page.getByText("per week")).toBeVisible();
await expect(page.locator(".price-row strong", { hasText: "$400" })).toBeVisible();
await expect(page.getByText("per hour")).toBeVisible();
await expect(page.getByText("Ten-hour minimum engagement")).toBeVisible();
await expect(page.getByText("open infrastructure and open specs for AI agent systems")).toBeVisible();
await expect(page.getByRole("button", { name: "Request review" })).toBeVisible();
await expect(page.getByPlaceholder("you@example.com")).toBeVisible();
await expect(page.getByPlaceholder("Describe the agent workflow")).toBeVisible();
await expect(page.getByText("without exposing merchant credentials to the browser")).toBeVisible();
await expect(page.getByText("COINPAY_PRODUCT=logicsrc-hire-us")).toBeVisible();
await expect(page.getByText("COINPAY_RATE_USD_PER_HOUR=400")).toBeVisible();
await expect(page.getByText("COINPAY_BILLING=metered_hours")).toBeVisible();
await expect(page.getByText("COINPAY_STATUS=pending_acceptance")).toBeVisible();
});

Expand Down
8 changes: 4 additions & 4 deletions apps/logicsrc-web/src/app/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { HomeInteractivity } from "@/components/home-interactivity";
// scrolled to the matching section. We preserve those URLs (they are canonical
// in sitemap.xml) by rendering the same page for each known route and 404ing
// anything else.
// /about and /docs are now real routes (app/about, app/docs); the rest still
// render the homepage SPA scrolled to their section.
// /about, /docs, /pricing, and /terms are now real routes (app/about, app/docs,
// app/pricing, app/terms); the rest still render the homepage SPA scrolled to
// their section.
const ROUTE_META: Record<string, { title: string; description: string }> = {
openspec: {
title: "LogicSRC vs OpenSpec.dev · LogicSRC",
Expand All @@ -21,9 +22,8 @@ const ROUTE_META: Record<string, { title: string; description: string }> = {
},
"hire-us": {
title: "Hire Us · LogicSRC",
description: "Implementation help for LogicSRC, AgentSwarm, and Credential Sharing at $250/week for accepted work, paid via CoinPay.",
description: "Implementation help for LogicSRC, AgentSwarm, and Credential Sharing at $400/hour for accepted work, paid via CoinPay.",
},
terms: { title: "Terms · LogicSRC", description: "LogicSRC terms of use." },
privacy: { title: "Privacy · LogicSRC", description: "LogicSRC privacy notes." },
"agent-swarm": {
title: "AgentSwarm · LogicSRC",
Expand Down
2 changes: 1 addition & 1 deletion apps/logicsrc-web/src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default function AboutPage(): ReactNode {

<h3>Work with us</h3>
<p>
Profullstack implements LogicSRC-based systems at $250/week for
Profullstack implements LogicSRC-based systems at $400/hour for
accepted work, paid via CoinPay. See <a href="/hire-us">Hire Us</a>.
</p>
</div>
Expand Down
46 changes: 39 additions & 7 deletions apps/logicsrc-web/src/app/api/hire-us/coinpay-checkout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,23 @@ import { choosePaymentRail, fetchMerchantEligibility, parseJson } from "@/lib/co

export const dynamic = "force-dynamic";

// POST /api/hire-us/coinpay-checkout — create a $250/week CoinPay checkout for
// the Hire Us plan, choosing card/crypto/both based on merchant eligibility.
// POST /api/hire-us/coinpay-checkout — create a CoinPay checkout for approved
// Hire Us hours at $400/hour, choosing card/crypto/both based on merchant
// eligibility. Billing is metered: the caller supplies the approved hours and the
// amount is derived from them, never a fixed recurring figure.
const RATE_USD_PER_HOUR = 400;
const MINIMUM_HOURS = 10;

// Hours are quoted in quarter-hour increments; anything finer is a rounding
// artifact rather than a real billing unit.
function parseHours(value: unknown): number | null {
const hours = typeof value === "number" ? value : Number(value);
if (!Number.isFinite(hours) || hours < MINIMUM_HOURS) return null;
const quarters = Math.round(hours * 4);
if (Math.abs(hours * 4 - quarters) > 1e-9) return null;
return quarters / 4;
}

export async function POST(request: NextRequest) {
const apiKey = process.env.COINPAY_API_KEY;
const eligibilityApiKey = process.env.COINPAY_ELIGIBILITY_API_KEY || process.env.COINPAY_AGENT_API_KEY || apiKey;
Expand All @@ -22,6 +37,19 @@ export async function POST(request: NextRequest) {
try {
const body = (await request.json().catch(() => ({}))) as Record<string, unknown>;
const buyerEmail = typeof body.email === "string" ? body.email.trim().slice(0, 160) : "";
const hours = body.hours === undefined ? MINIMUM_HOURS : parseHours(body.hours);

if (hours === null) {
return json(
{
success: false,
error: `Approved hours must be a quarter-hour increment of at least ${MINIMUM_HOURS}`
},
422
);
}

const amountUsdDue = Math.round(hours * RATE_USD_PER_HOUR * 100) / 100;
const eligibility = await fetchMerchantEligibility(apiUrl, eligibilityApiKey, eligibilityMerchantId);
const paymentRail = choosePaymentRail(eligibility, blockchain);

Expand All @@ -37,18 +65,20 @@ export async function POST(request: NextRequest) {
},
body: JSON.stringify({
business_id: businessId,
amount_usd: 250,
amount_usd: amountUsdDue,
payment_method: paymentRail.method,
currency: paymentRail.currency,
...(paymentRail.blockchain ? { blockchain: paymentRail.blockchain } : {}),
description: "LogicSRC Hire Us - $250/week",
description: `LogicSRC Hire Us - ${hours}h @ $${RATE_USD_PER_HOUR}/hour`,
success_url: `${publicUrl}/hire-us?payment=success`,
cancel_url: `${publicUrl}/hire-us?payment=cancelled`,
redirect_url: `${publicUrl}/hire-us?payment=coinpay`,
webhook_url: `${publicUrl}/api/webhooks/coinpay`,
metadata: {
product: "logicsrc-hire-us",
interval: "week",
billing: "metered_hours",
hours,
rate_usd_per_hour: RATE_USD_PER_HOUR,
source: "logicsrc.com/hire-us",
...(buyerEmail ? { buyer_email: buyerEmail } : {})
}
Expand All @@ -70,13 +100,15 @@ export async function POST(request: NextRequest) {
}

const payment = (payload.payment as Record<string, unknown>) || {};
const amountUsd = Number(payment.amount_usd ?? payment.amount ?? 250);
const amountUsd = Number(payment.amount_usd ?? payment.amount ?? amountUsdDue);
return json(
{
success: true,
payment: {
id: payment.id,
amount_usd: Number.isFinite(amountUsd) ? amountUsd : 250,
amount_usd: Number.isFinite(amountUsd) ? amountUsd : amountUsdDue,
hours,
rate_usd_per_hour: RATE_USD_PER_HOUR,
payment_method: payment.stripe_checkout_url ? "card" : paymentRail.method,
currency: payment.currency ?? payment.blockchain ?? paymentRail.blockchain ?? paymentRail.currency,
crypto_amount: payment.amount_crypto ?? payment.crypto_amount ?? null,
Expand Down
15 changes: 10 additions & 5 deletions apps/logicsrc-web/src/app/api/hire-us/project-request/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import { json } from "@/lib/http";

export const dynamic = "force-dynamic";

// POST /api/hire-us/project-request — accept a Hire Us project request before a
// recurring CoinPay invoice is created (invoice is created after acceptance).
// POST /api/hire-us/project-request — accept a Hire Us project request before any
// CoinPay invoice is created. Hire Us bills metered hours at $400/hour, so there is
// no amount until we accept the project and hours are approved.
const RATE_USD_PER_HOUR = 400;
const MINIMUM_HOURS = 10;

export async function POST(request: NextRequest) {
try {
const body = (await request.json().catch(() => ({}))) as Record<string, unknown>;
Expand All @@ -20,7 +24,7 @@ export async function POST(request: NextRequest) {
id: requestId,
contact,
project_length: project.length,
plan: "250/week",
plan: "400/hour",
invoice: "pending_acceptance"
});

Expand All @@ -30,8 +34,9 @@ export async function POST(request: NextRequest) {
request: {
id: requestId,
status: "pending_acceptance",
amount_usd: 250,
interval: "week",
rate_usd_per_hour: RATE_USD_PER_HOUR,
billing: "metered_hours",
minimum_hours: MINIMUM_HOURS,
invoice: "created_after_acceptance"
}
},
Expand Down
2 changes: 1 addition & 1 deletion apps/logicsrc-web/src/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function GET(): Response {
## Company & legal

- [About](${SITE_URL}/about): What LogicSRC is and who maintains it (Profullstack, Inc.).
- [Hire Us](${SITE_URL}/hire-us): Implementation help at $250/week for accepted LogicSRC work.
- [Hire Us](${SITE_URL}/hire-us): Implementation help at $400/hour for accepted LogicSRC work.
- [Terms](${SITE_URL}/terms)
- [Privacy](${SITE_URL}/privacy)
`;
Expand Down
14 changes: 8 additions & 6 deletions apps/logicsrc-web/src/app/pricing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { SiteShell } from "@/components/site-shell";
export const metadata: Metadata = {
title: "Pricing · LogicSRC",
description:
"LogicSRC the open specification, schemas, SDKs, and CLI are free and open source. Implementation help is $250/week for accepted work, paid via CoinPay.",
"LogicSRC the open specification, schemas, SDKs, and CLI are free and open source. Implementation help is $400/hour for accepted work, paid via CoinPay.",
alternates: { canonical: "/pricing" },
};

Expand All @@ -16,15 +16,15 @@ const FAQ: Array<{ q: string; a: string }> = [
},
{
q: "How does pricing work?",
a: "The standard is free. If you want Profullstack to build a LogicSRC-based system for you, implementation work is billed at $250/week for accepted work, paid via a CoinPay recurring invoice.",
a: "The standard is free. If you want Profullstack to build a LogicSRC-based system for you, implementation work is billed at $400/hour against actual hours worked, invoiced through CoinPay after you approve them. The minimum engagement is 10 hours.",
},
{
q: "Who is LogicSRC for?",
a: "Engineering and AI-platform teams building systems where humans and AI agents coordinate — boards, tasks, agent runs, identity, payments, and audit — without locking into a single vendor's proprietary platform.",
},
{
q: "How do I pay or get started?",
a: "Read the docs and adopt the schemas for free, or submit a project through the Hire Us form. Accepted projects are invoiced weekly via CoinPay.",
a: "Read the docs and adopt the schemas for free, or submit a project through the Hire Us form. Accepted projects are invoiced for approved hours via CoinPay.",
},
];

Expand Down Expand Up @@ -61,9 +61,11 @@ export default function PricingPage(): ReactNode {
CLI, TUI, and reference plugins are open source.
</li>
<li>
<strong>Implementation — $250/week.</strong> Profullstack builds
LogicSRC-based systems for accepted projects, billed weekly via
CoinPay. See <a href="/hire-us">Hire Us</a>.
<strong>Implementation — $400/hour.</strong> Profullstack builds
LogicSRC-based systems for accepted projects, billed against actual
hours worked and invoiced via CoinPay once you approve them.
Ten-hour minimum engagement. See <a href="/hire-us">Hire Us</a> and{" "}
<a href="/terms">Terms</a>.
</li>
</ul>

Expand Down
2 changes: 1 addition & 1 deletion apps/logicsrc-web/src/app/skill.md/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Base URL: ${SITE_URL}
- Read the LogicSRC coordination schemas and conventions.
- Compare LogicSRC with OpenSpec.dev.
- Request paid implementation help via the Hire Us flow (${SITE_URL}/hire-us),
billed at $250/week for accepted work and paid through CoinPay.
billed at $400/hour for accepted work and paid through CoinPay.

## Notes

Expand Down
Loading
Loading