Skip to content

Commit b7f9de1

Browse files
ktamas77claude
andcommitted
Add TypeScript toolchain: signing helper, tests, husky + CI
- src/onsight.ts: typed helper for the wallet-signature owner-action scheme (signOwnerActionHeaders / ownerActionMessage / missionOrderUrl), so agent authors don't reimplement replay-safe signing. - src/onsight.test.ts: jest unit tests that mirror the server's WalletSigGuard message construction, so they fail if the client ever drifts. - Convert the x402 example to TS, importing the helper. - eslint + prettier + tsc + jest, run in parallel as a husky pre-commit hook and as four parallel PR/push CI jobs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dcb9f74 commit b7f9de1

15 files changed

Lines changed: 13152 additions & 87 deletions

.github/workflows/ci.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
8+
# Four independent jobs → GitHub runs them in parallel.
9+
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-node@v4
15+
with: { node-version: 22, cache: npm }
16+
- run: npm ci
17+
- run: npm run lint
18+
19+
format:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v4
23+
- uses: actions/setup-node@v4
24+
with: { node-version: 22, cache: npm }
25+
- run: npm ci
26+
- run: npm run format:check
27+
28+
typecheck:
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@v4
32+
- uses: actions/setup-node@v4
33+
with: { node-version: 22, cache: npm }
34+
- run: npm ci
35+
- run: npm run typecheck
36+
37+
test:
38+
runs-on: ubuntu-latest
39+
steps:
40+
- uses: actions/checkout@v4
41+
- uses: actions/setup-node@v4
42+
with: { node-version: 22, cache: npm }
43+
- run: npm ci
44+
- run: npm test

.husky/pre-commit

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Run the four checks in parallel; fail the commit if any one fails.
2+
npm run lint & p1=$!
3+
npm run format:check & p2=$!
4+
npm run typecheck & p3=$!
5+
npm test & p4=$!
6+
7+
fail=0
8+
for p in $p1 $p2 $p3 $p4; do
9+
wait "$p" || fail=1
10+
done
11+
exit $fail

.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
dist
3+
package-lock.json

README.md

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ Live at **[onsight.photo](https://onsight.photo)**.
1313

1414
Onsight is a crowdsourced ground-truth photography marketplace:
1515

16-
- A **requester** posts a *mission* — "photograph the queue outside this address,
16+
- A **requester** posts a _mission_ — "photograph the queue outside this address,
1717
today, from the street" — and funds a reward in USDC.
1818
- Local **photographers** nearby fulfill it and submit photos.
1919
- The requester picks a winner; the escrowed reward pays out on-chain.
2020

2121
There are **two front doors onto the same rail**:
2222

23-
| Requester | How they order | How they pay |
24-
|---|---|---|
25-
| **Human** | web UI at [onsight.photo](https://onsight.photo) | direct USDC deposit (card on-ramp available) |
26-
| **AI agent** | `POST /agent/missions` | **[x402](https://x402.org)** — pays USDC per call, no account, no key |
23+
| Requester | How they order | How they pay |
24+
| ------------ | ------------------------------------------------ | --------------------------------------------------------------------- |
25+
| **Human** | web UI at [onsight.photo](https://onsight.photo) | direct USDC deposit (card on-ramp available) |
26+
| **AI agent** | `POST /agent/missions` | **[x402](https://x402.org)** — pays USDC per call, no account, no key |
2727

2828
Same escrow, same photographer supply, same payout. Only how the order is placed
2929
and paid differs.
@@ -63,13 +63,13 @@ payments, since the money is already escrowed.
6363

6464
Base URL: `https://onsight.photo`
6565

66-
| Agent action | Endpoint | Auth |
67-
|---|---|---|
68-
| Post + fund a mission (place a photo order) | `POST /agent/missions?rewardUsd=N` | **x402** (payment = escrow funding) |
69-
| Read a mission + submitted photos | `GET /agent/missions/:id` | public |
70-
| Update brief / extend dates | `PATCH /agent/missions/:id` | wallet signature |
71-
| Pick + pay a winner | `POST /agent/missions/:id/select` `{ photoId }` | wallet signature |
72-
| Refund / cancel | `POST /agent/missions/:id/refund` | wallet signature |
66+
| Agent action | Endpoint | Auth |
67+
| ------------------------------------------- | ----------------------------------------------- | ----------------------------------- |
68+
| Post + fund a mission (place a photo order) | `POST /agent/missions?rewardUsd=N` | **x402** (payment = escrow funding) |
69+
| Read a mission + submitted photos | `GET /agent/missions/:id` | public |
70+
| Update brief / extend dates | `PATCH /agent/missions/:id` | wallet signature |
71+
| Pick + pay a winner | `POST /agent/missions/:id/select` `{ photoId }` | wallet signature |
72+
| Refund / cancel | `POST /agent/missions/:id/refund` | wallet signature |
7373

7474
**Wallet-signature auth** (owner actions): send `X-Wallet-Address`,
7575
`X-Wallet-Signature`, and `X-Wallet-Timestamp`, signing the message
@@ -102,16 +102,34 @@ curl -i -X POST "https://onsight.photo/agent/missions?rewardUsd=20" \
102102
To actually place and fund the order, an agent signs the payment and retries with an
103103
`X-PAYMENT` header. The full runnable flow — using [`x402-fetch`](https://www.npmjs.com/package/x402-fetch)
104104
so payment is handled automatically — is in
105-
**[`examples/x402-agent-order.js`](examples/x402-agent-order.js)**.
105+
**[`examples/x402-agent-order.ts`](examples/x402-agent-order.ts)**:
106+
107+
```bash
108+
npm install
109+
AGENT_PRIVATE_KEY=0x... npm run example
110+
```
106111

107112
Buying as a human? See **[`examples/human-order.md`](examples/human-order.md)**.
108113

109114
## Examples
110115

111-
- [`examples/x402-agent-order.js`](examples/x402-agent-order.js) — an agent places and
112-
funds a photo order via x402, then reads it back.
116+
- [`examples/x402-agent-order.ts`](examples/x402-agent-order.ts) — an agent places and
117+
funds a photo order via x402, reads it back, then picks a winner with a signed
118+
owner action.
113119
- [`examples/human-order.md`](examples/human-order.md) — walkthrough for human buyers.
114120

121+
## Signing helper
122+
123+
Owner actions (pick a winner, refund, edit a brief) are authorized by signing a
124+
canonical, body-bound message — get the format wrong and the server rejects you, or
125+
you sign something you didn't mean to. [`src/onsight.ts`](src/onsight.ts) encodes it
126+
once (`signOwnerActionHeaders`, `ownerActionMessage`, `missionOrderUrl`), and its
127+
tests mirror the server guard so they break loudly if the scheme ever drifts.
128+
129+
```bash
130+
npm run lint && npm run typecheck && npm test # or all four via the pre-commit hook
131+
```
132+
115133
## Status
116134

117135
The USDC escrow rail is live on Base mainnet; the x402 agent channel is rolling out.

eslint.config.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import js from "@eslint/js";
2+
import tseslint from "typescript-eslint";
3+
4+
export default tseslint.config(
5+
{ ignores: ["node_modules", "dist"] },
6+
js.configs.recommended,
7+
...tseslint.configs.recommended,
8+
{
9+
languageOptions: {
10+
globals: { process: "readonly", console: "readonly", fetch: "readonly" },
11+
},
12+
},
13+
);

examples/human-order.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ The web app wraps the same escrow rail the agent API uses.
77

88
1. **Go to [onsight.photo](https://onsight.photo)** and start a new mission.
99
2. **Describe what you need.** A good brief is specific:
10-
- *What* to photograph (a storefront, a line, a shelf, a view).
11-
- *Where* — an address or a dropped map pin, plus how far around it is acceptable.
12-
- *When* — "today", "during business hours", a specific date/time window.
10+
- _What_ to photograph (a storefront, a line, a shelf, a view).
11+
- _Where_ — an address or a dropped map pin, plus how far around it is acceptable.
12+
- _When_ — "today", "during business hours", a specific date/time window.
1313
- Any constraints — daylight, no faces, a particular angle.
1414
3. **Set the reward.** This is what the winning photographer earns. Higher rewards
1515
and tighter deadlines get filled faster.

examples/package.json

Lines changed: 0 additions & 10 deletions
This file was deleted.

examples/x402-agent-order.js

Lines changed: 0 additions & 59 deletions
This file was deleted.

examples/x402-agent-order.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Place and fund a ground-truth photo order on Onsight, paid via x402 —
2+
// then (optionally) pick the winning photo with a wallet-signed owner action.
3+
//
4+
// An AI agent can't photograph a real place; this commissions a real person to.
5+
// x402 handles payment inline: the first request comes back 402, `x402-fetch`
6+
// signs a USDC payment on Base and retries automatically. The agent's wallet IS
7+
// the account — no signup, no API key.
8+
//
9+
// Run (from the repo root):
10+
// npm install
11+
// AGENT_PRIVATE_KEY=0x... npm run example
12+
//
13+
// The wallet needs USDC on Base (or Base Sepolia for testing) plus a little ETH
14+
// for the on-chain settlement.
15+
16+
import { wrapFetchWithPayment } from "x402-fetch";
17+
import { privateKeyToAccount } from "viem/accounts";
18+
import { missionOrderUrl, signOwnerActionHeaders } from "../src/onsight.js";
19+
20+
const BASE_URL = process.env.ONSIGHT_URL ?? "https://onsight.photo";
21+
const REWARD_USD = 20;
22+
23+
const key = process.env.AGENT_PRIVATE_KEY;
24+
if (!key)
25+
throw new Error("Set AGENT_PRIVATE_KEY to a wallet holding USDC on Base");
26+
27+
const account = privateKeyToAccount(key as `0x${string}`);
28+
// fetch that transparently answers a 402 by paying and retrying:
29+
const fetchWithPay = wrapFetchWithPayment(fetch, account);
30+
31+
const mission = {
32+
title: "Storefront at 123 Market St",
33+
description: "Street-level photo of the entrance, taken today in daylight.",
34+
lat: 37.7897,
35+
lng: -122.3972,
36+
radiusMeters: 150,
37+
};
38+
39+
// 1. Post + fund the mission. First call → 402; x402-fetch pays and retries.
40+
const res = await fetchWithPay(missionOrderUrl(REWARD_USD, BASE_URL), {
41+
method: "POST",
42+
headers: { "Content-Type": "application/json" },
43+
body: JSON.stringify(mission),
44+
});
45+
if (!res.ok) throw new Error(`Order failed: ${res.status} ${await res.text()}`);
46+
47+
const created = (await res.json()) as {
48+
id: string;
49+
txHash?: string;
50+
paymentTxHash?: string;
51+
};
52+
console.log("Mission funded:", created.id);
53+
console.log("Settlement tx:", created.txHash ?? created.paymentTxHash);
54+
55+
// 2. Read it back (public — no payment, no signature) to poll for submitted photos.
56+
const check = await fetch(`${BASE_URL}/agent/missions/${created.id}`);
57+
const state = (await check.json()) as {
58+
status?: string;
59+
paymentStatus?: string;
60+
photos?: unknown[];
61+
};
62+
console.log(
63+
`Status: ${state.status ?? state.paymentStatus}${(state.photos ?? []).length} photo(s) so far`,
64+
);
65+
66+
// 3. Pick + pay a winner. This is an OWNER action — no more x402, just prove
67+
// control of the funding wallet by signing the request. Guarded behind an env
68+
// var so running the example doesn't accidentally pay out.
69+
const photoId = process.env.SELECTED_PHOTO_ID;
70+
if (photoId) {
71+
const path = `/agent/missions/${created.id}/select`;
72+
const body = { photoId };
73+
const headers = await signOwnerActionHeaders(account, "POST", path, body);
74+
const pick = await fetchWithPay(`${BASE_URL}${path}`, {
75+
method: "POST",
76+
headers: { "Content-Type": "application/json", ...headers },
77+
body: JSON.stringify(body),
78+
});
79+
console.log(
80+
pick.ok ? `Paid out to photo ${photoId}` : `Select failed: ${pick.status}`,
81+
);
82+
}

jest.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
export default {
3+
preset: "ts-jest/presets/default-esm",
4+
testEnvironment: "node",
5+
moduleNameMapper: { "^(\\.{1,2}/.*)\\.js$": "$1" },
6+
testMatch: ["**/*.test.ts"],
7+
};

0 commit comments

Comments
 (0)