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
66 changes: 66 additions & 0 deletions .agent/context/20260912T172804Z-clarify-payment-services.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Session: clarify payment service labels

## Date/time

2026-09-12 17:28 UTC.

## User goal and original request

Rename the website surfaces that currently imply two external API products. The
first flow is a direct Arc Testnet USDC payment with a team-operated sample
result. The second flow buys a dataset from OneShot's own x402 seller.

## Assumptions and acceptance

- Change user-facing copy and matching tests only.
- Preserve tool identifiers, API contracts, payment behavior, recipients, and
settlement logic.
- Label the team-operated x402 seller explicitly.
- Keep the direct payment purpose as the existing `report_subject` payload field.

## Plan and decisions

- Rename `API services` to `Payment services`.
- Rename `Company research service` to `Direct Arc payment`.
- Rename `Company or domain` to `Payment purpose`.
- Rename `Circle Dataset API` to `OneShot x402 Dataset` and identify it as a
team-operated demo.

## Files/components touched

- `apps/web/src/App.tsx`
- `apps/web/src/components/JobWorkspace.tsx`
- `apps/web/src/components/workspace-copy.ts`
- Focused web unit and browser tests.

## Commands/checks

- `pnpm --filter @oneshot/web exec vitest run --config vitest.config.ts --pool=threads --maxWorkers=1 --no-file-parallelism` — PASS (18 files, 83 tests).
- `pnpm --filter @oneshot/web typecheck` — PASS after clearing incomplete generated output left by a full disk.
- `pnpm lint` — PASS.
- `pnpm format:check` — PASS.
- `pnpm --filter @oneshot/web test:browser` — PASS (8 Playwright tests).
- `git diff --check` — PASS.

## External-doc findings

None. This change follows current repository behavior.

## Unresolved questions

None for this copy-only scope.

## Branch/commit/PR state

- Branch: `fix/clarify-payment-services`
- Base: `origin/develop` at `ff146e49407b078183ed619ff3f3b2e11b17da2c`.
- Commit and PR: pending.

## Gate state

- Gate A: explicitly waived by the user on 2026-09-12.
- Gate B: explicitly waived by the user on 2026-09-12.

## Handoff/next steps

Commit the validated candidate, push the branch, and open a PR for human review.
18 changes: 9 additions & 9 deletions apps/web/browser/p5.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ async function mockJobApi(
async function unlockWorkspace(page: Page): Promise<void> {
await page.getByText('Machine token (advanced)').click();
await page.getByLabel('Machine token').fill('browser-memory-token');
await expect(page.getByRole('tab', { name: 'API services' })).toBeVisible();
await expect(page.getByRole('tab', { name: 'Payment services' })).toBeVisible();
}

test.describe('resumable job workspace', () => {
Expand All @@ -123,24 +123,24 @@ test.describe('resumable job workspace', () => {
const calls = await mockJobApi(page, { startDelayMs: 1000, resumeDelayMs: 1000 });
await page.goto('/app');
await unlockWorkspace(page);
await page.getByRole('tab', { name: 'API services' }).click();
await page.getByLabel('Company or domain').fill('acme.com');
await page.getByRole('tab', { name: 'Payment services' }).click();
await page.getByLabel('Payment purpose').fill('acme.com');
await page
.getByLabel('Service destination wallet')
.fill('0x1111111111111111111111111111111111111111');
await page.getByLabel('Amount (USDC)').fill('2.5');
await page.getByText('Request key (advanced)').click();
await expect(page.locator('#generated-task-key')).toHaveValue(/report-acme-com-/u);
await page
.getByRole('region', { name: 'Company research service' })
.getByRole('region', { name: 'Direct Arc payment' })
.getByRole('button', { name: 'Review payment details' })
.click();
await expect.poll(() => calls.filter((call) => call === 'POST /v1/jobs/quote')).toHaveLength(1);
expect(calls).not.toContain('POST /v1/jobs');
await expect(page.getByRole('heading', { name: 'Review before approval' })).toBeVisible();
await expect(page.getByText('Nothing has been paid yet.')).toBeVisible();
const approve = page
.getByRole('region', { name: 'Company research service' })
.getByRole('region', { name: 'Direct Arc payment' })
.locator('button')
.last();
await approve.click();
Expand Down Expand Up @@ -233,19 +233,19 @@ for (const theme of ['light', 'dark'] as const) {
expect(identity).not.toBeNull();
expect(header?.x).toBeCloseTo(identity!.x, 0);
expect(header?.width).toBeCloseTo(identity!.width, 0);
for (const label of ['Overview', 'API services', 'Requests', 'Payment proof']) {
for (const label of ['Overview', 'Payment services', 'Requests', 'Payment proof']) {
await page.getByRole('tab', { name: label, exact: true }).click();
await page.getByRole('tab', { name: label, exact: true }).hover();
await page.getByRole('tab', { name: label, exact: true }).focus();
if (label === 'API services') {
if (label === 'Payment services') {
await page.getByText('Request key (advanced)').click();
await page.getByLabel('Company or domain').fill('acme.com');
await page.getByLabel('Payment purpose').fill('acme.com');
await page
.getByLabel('Service destination wallet')
.fill('0x1111111111111111111111111111111111111111');
await page.getByLabel('Amount (USDC)').fill('2.5');
await page
.getByRole('region', { name: 'Company research service' })
.getByRole('region', { name: 'Direct Arc payment' })
.getByRole('button', { name: 'Review payment details' })
.click();
await expect(page.getByRole('heading', { name: 'Review before approval' })).toBeVisible();
Expand Down
24 changes: 12 additions & 12 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ function LandingPage(props: { readonly theme: Theme; readonly onToggleTheme: ()
<p className="eyebrow">RESUMABLE PAID SERVICES / ARC TESTNET</p>
<h1>Resume the job, not the payment.</h1>
<p className="hero-lead">
Approve one company-data report. If an agent restarts, the original task, payment
evidence and supplier result stay together.
Approve one direct Arc payment or paid x402 request. If an agent restarts, the original
task, payment evidence and result stay together.
</p>
<p className="hero-sublead">
One job. Many retries. At most one committed settlement. Team-operated testnet
Expand Down Expand Up @@ -149,7 +149,7 @@ function CabinetPage(props: {
const [activityError, setActivityError] = useState<string | null>(null);
const labels = {
overview: 'Overview',
services: 'API services',
services: 'Payment services',
requests: 'Requests',
protection: 'Payment proof',
} as const;
Expand Down Expand Up @@ -203,24 +203,24 @@ function CabinetPage(props: {
the permitted amount and recipient.
</li>
<li>
<strong>Prepare a request.</strong> Open API services. For the team report, enter a
subject, recipient and amount, then review payment details. Circle Dataset API gets
its price from the service.
<strong>Prepare a request.</strong> Open Payment services. For a direct Arc payment,
enter its purpose, recipient and amount, then review the details. OneShot x402 Dataset
gets its price from the team-operated demo seller.
</li>
<li>
<strong>Approve deliberately.</strong> Read “Recipient receives”, the destination and
Arc Testnet network. Keep the request key. Only the explicit approval button starts a
payment request.
</li>
<li>
<strong>Read the result.</strong> Open Requests for a team report. For Circle Dataset
API, use Check payment status in its service card. Inspect the actual payment state
and result.
<strong>Read the result.</strong> Open Requests for a direct Arc payment. For OneShot
x402 Dataset, use Check payment status in its service card. Inspect the actual payment
state and result.
</li>
<li>
<strong>Demonstrate recovery.</strong> For a team report, resume the existing result
from Requests. For Circle, replay the same request only when its payment is confirmed.
An uncertain payment needs investigation, not a new key.
<strong>Demonstrate recovery.</strong> For a direct Arc payment, resume the existing
sample result from Requests. For x402, replay the same request only when its payment is
confirmed. An uncertain payment needs investigation, not a new key.
</li>
</ol>
<p>
Expand Down
29 changes: 15 additions & 14 deletions apps/web/src/components/JobWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,18 +259,18 @@ export function JobWorkspace(props: {
return (
<section
className="panel job-workspace"
aria-label="Company research service"
aria-label="Direct Arc payment"
aria-busy={quoteLoading || starting}
>
<header>
<p className="eyebrow">API SERVICE</p>
<h2>Company research service</h2>
<p className="eyebrow">DIRECT PAYMENT</p>
<h2>Direct Arc payment</h2>
<p>
Prepare a team-operated company report request. You choose the destination and payment
amount; preview confirms those details before you approve a payment.
Send USDC directly to a reviewed recipient on Arc Testnet. OneShot keeps the payment and
its team-operated sample result bound to one request across retries.
</p>
</header>
<label htmlFor="report-subject">Company or domain</label>
<label htmlFor="report-subject">Payment purpose</label>
<input
id="report-subject"
disabled={quoteLoading || starting}
Expand All @@ -279,7 +279,7 @@ export function JobWorkspace(props: {
setSubject(event.target.value);
clearQuote();
}}
placeholder="acme.com"
placeholder="Q4 supplier research"
/>
<label htmlFor="report-recipient">Service destination wallet</label>
<input
Expand Down Expand Up @@ -544,18 +544,19 @@ export function CircleX402DemoPanel(props: {
}

return (
<section className="panel paid-api-panel" aria-label="Circle Dataset API service">
<section className="panel paid-api-panel" aria-label="OneShot x402 Dataset service">
<header className="panel-heading">
<div>
<p className="eyebrow">CONNECTED API SERVICE</p>
<h2>Circle Dataset API</h2>
<p className="eyebrow">TEAM-OPERATED X402 DEMO</p>
<h2>OneShot x402 Dataset</h2>
</div>
<span className="badge tone-neutral">Arc Testnet</span>
</header>
<p>
Get a dataset result through Circle’s payment rail. OneShot keeps one request key so a retry
reuses the original payment instead of charging twice. The connected wallet signs the exact
payment to the API seller; OneShot never substitutes its own wallet.
Buy a demo dataset from OneShot’s own seller through Circle x402. OneShot keeps one request
key so a retry reuses the original payment instead of charging twice. When a connected
wallet is used, it signs the exact payment to the API seller; OneShot never substitutes its
own wallet.
</p>
{props.userWallet && (
<p className="field-help">
Expand Down Expand Up @@ -828,7 +829,7 @@ export function JobList(props: {
{loading ? (
<p role="status">Checking requests…</p>
) : jobs.length === 0 ? (
<p>No requests yet. Open API services to start a supported request.</p>
<p>No requests yet. Open Payment services to start a supported request.</p>
) : (
<ul className="attempts job-list">
{jobs.map((job, index) => {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/workspace-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ export function deliveryStatusCopy(state: DeliveryState): StatusCopy {
export function serviceLabel(toolId: string): string {
switch (toolId) {
case 'circle-x402-api-v1':
return 'Circle Dataset API';
return 'OneShot x402 Dataset';
case 'team-report-v1':
return 'Company research service';
return 'Direct Arc payment';
default:
return 'Paid API service';
}
Expand Down
12 changes: 6 additions & 6 deletions apps/web/test/app-composition.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('Gate P5 shell composition', () => {
/>,
);
expect(screen.getByRole('heading', { name: 'Your payment workspace', level: 1 })).toBeTruthy();
expect(screen.getByRole('tab', { name: 'API services' })).toBeTruthy();
expect(screen.getByRole('tab', { name: 'Payment services' })).toBeTruthy();
expect(screen.getByRole('tab', { name: 'Requests' })).toBeTruthy();
expect(screen.getByRole('tab', { name: 'Payment proof' })).toBeTruthy();
});
Expand All @@ -67,7 +67,7 @@ describe('Gate P5 shell composition', () => {

expect(screen.getAllByRole('tab').map((tab) => tab.textContent)).toEqual([
'Overview',
'API services',
'Payment services',
'Requests',
'Payment proof',
]);
Expand All @@ -77,7 +77,7 @@ describe('Gate P5 shell composition', () => {
expect(screen.queryByRole('tab', { name: 'Team & access' })).toBeNull();
});

it('gives API services and Requests distinct responsibilities', async () => {
it('gives Payment services and Requests distinct responsibilities', async () => {
const user = userEvent.setup();
const jobClient = {
async list() {
Expand Down Expand Up @@ -120,14 +120,14 @@ describe('Gate P5 shell composition', () => {
/>,
);

await user.click(screen.getByRole('tab', { name: 'API services' }));
expect(screen.getByRole('heading', { name: 'Company research service' })).toBeTruthy();
await user.click(screen.getByRole('tab', { name: 'Payment services' }));
expect(screen.getByRole('heading', { name: 'Direct Arc payment' })).toBeTruthy();
expect(screen.queryByRole('heading', { name: 'Requests and results', level: 2 })).toBeNull();
await user.click(screen.getByRole('tab', { name: 'Requests' }));
expect(
await screen.findByRole('heading', { name: 'Requests and results', level: 2 }),
).toBeTruthy();
expect(screen.getByText(/Open API services to start/u)).toBeTruthy();
expect(screen.getByText(/Open Payment services to start/u)).toBeTruthy();
});

it('mounts A05, B05, and C05 without a settlement bypass', async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/test/components.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ describe('JobWorkspace payment inputs', () => {
};
render(<JobWorkspace client={client as never} onSelectIntent={() => undefined} />);

await user.type(screen.getByLabelText('Company or domain'), 'acme.com');
await user.type(screen.getByLabelText('Payment purpose'), 'acme.com');
await user.type(
screen.getByLabelText('Service destination wallet'),
'0x2222222222222222222222222222222222222222',
Expand Down
Loading