Skip to content

Commit 5ad628d

Browse files
sadeshmukhclaude
andauthored
Add submission & review system (#8)
* Add submission & review system - Wire up the ship button in projects page to call submit API - Add /review dashboard for admins to review pending submissions - Implement buff system with live M calculation and payout preview - Add review API endpoint with approve/reject actions - Post review results to Slack #raspapi-logs - Update .env.example with ADMIN_IDS, SLACK_BOT_TOKEN, SLACK_LOGS_CHANNEL https://claude.ai/code/session_019nt7obPSBSVJKfnf5RDA6U * fix: apply biome formatting https://claude.ai/code/session_019nt7obPSBSVJKfnf5RDA6U * Hedging my ledger * DM approval/rejection status, better logging * Format, buff it up * Drop 5-7 explicit hour req --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7bc2429 commit 5ad628d

14 files changed

Lines changed: 1854 additions & 20 deletions

File tree

.env.example

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,12 @@ PUBLIC_BASE_URL=http://localhost:4321
1212

1313
# readonly is fine
1414
AIRTABLE_API_KEY=pat-
15-
AIRTABLE_BASE_ID=app-
15+
AIRTABLE_BASE_ID=app-
16+
AIRTABLE_SUBMISSIONS_TABLE_ID=tbl-
17+
18+
# comma-separated Slack IDs for admin/reviewer access
19+
ADMIN_IDS=U123,U456
20+
21+
# review
22+
SLACK_BOT_TOKEN=xoxb-
23+
SLACK_LOGS_CHANNEL=C0123456789

bun.lockb

1.5 KB
Binary file not shown.

manifest.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
display_information:
2+
name: RaspAPI
3+
4+
features:
5+
bot_user:
6+
display_name: RaspAPI
7+
always_online: false
8+
9+
oauth_config:
10+
scopes:
11+
bot:
12+
- chat:write
13+
- im:write

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@astrojs/node": "^9.5.1",
1717
"@tailwindcss/vite": "^4.1.18",
1818
"astro": "^5.16.8",
19+
"slack.ts": "0.0.15",
1920
"tailwindcss": "^4.1.18",
2021
"three": "^0.182.0",
2122
"typescript": "^5.9.3"

src/content/guides/about.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ Before submitting your project, make sure you meet the following requirements:
2020
- Your API should include docs at `/docs`. These can be handwritten or auto-generated (Swagger, Redoc, Scalar, etc.), but should clearly explain your endpoints and give an example of a valid request for each.
2121
- Your API should be usable by anyone. If it requires auth or API keys, make sure there's a way for people to easily get access, such as with a demo key on your docs.
2222
- Your code should be in a public git repo with a (hand-written, please) README explaining what it is.
23-
- You should have around 5–7 hours tracked on Hackatime.
2423
- Make something cool and have fun!
24+
25+
Reviewers award a multiplier based on what your API implements - see the [Buffs & Multipliers](/guides/buffs) guide for details on how payouts are calculated.

src/content/guides/buffs.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
title: Buffs & Multipliers
3+
description: How the review multiplier system works
4+
order: 1
5+
---
6+
7+
*Note: these values are subject to change as we iterate.*
8+
9+
When your submission is reviewed, a reviewer assigns a **multiplier** based on what your API implements. The multiplier directly scales your payout:
10+
11+
> **Payout = hours × 4 × multiplier**
12+
13+
So if you logged 6 hours and your multiplier is 1.25, you'd receive 30 raspberries.
14+
15+
## How the Multiplier Is Built
16+
17+
The multiplier starts at **1.0** and is built in two stages:
18+
19+
1. **Flat bonuses** are added together (capped at 1.5 total)
20+
2. **Multiplier buffs** are applied on top of the capped value
21+
22+
---
23+
24+
## Flat Bonuses
25+
26+
These are additive. Reviewers check your project for each one and add the bonus if it qualifies.
27+
28+
| Buff | Bonus |
29+
|------|-------|
30+
| Authorization (JWT, user tokens, API keys) | +0.15 |
31+
| Persistence (database, file storage) | +0.10 |
32+
| External API integration (non-trivial) | +0.10 |
33+
| Rate limiting / abuse prevention | +0.10 |
34+
| Pagination | +0.05 |
35+
| Proper error handling & input validation | +0.05 |
36+
| Additional endpoints beyond the minimum, up to 4 | +0.03 each |
37+
38+
The sum of all flat bonuses is **capped at 1.5** before multiplier buffs are applied. If your bonuses add up to 1.6, they'll be treated as 1.5.
39+
40+
---
41+
42+
## Multiplier Buffs
43+
44+
These are applied after the flat bonus cap and stack multiplicatively.
45+
46+
| Buff | Multiplier |
47+
|------|------------|
48+
| Cool project | ×1.3 |
49+
| Exceptional code quality / architecture | ×1.2 |
50+
51+
---
52+
53+
## Example
54+
55+
Say your API has:
56+
57+
- Auth (+0.15)
58+
- A database (+0.10)
59+
- Rate limiting (+0.10)
60+
- Solid error handling (+0.05)
61+
- 2 extra endpoints (+0.06)
62+
63+
That's **1.0 + 0.46 = 1.46**. Below the 1.5 cap, so no clipping.
64+
65+
If the reviewer also thinks it's a cool project (×1.3):
66+
67+
**1.46 × 1.3 = 1.898**
68+
69+
With 6 hours logged: **6 × 4 × 1.898 = ~46 raspberries**.
70+
71+
---
72+
73+
Buffs are awarded at reviewer discretion. Please do not ask for specific buffs in your submission notes, but do make sure to highlight any features you implemented that you think are cool and deserve recognition!

src/content/guides/building/c-mongoose.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ However, real APIs usually have multiple endpoints and different request types l
130130
131131
The parsed HTTP request stored in struct mg_http_message contains two important fields:
132132
133-
- `hm->uri` the request path (for example /api/hello)
134-
- `hm->method` the HTTP method (GET, POST, etc.)
133+
- `hm->uri` - the request path (for example /api/hello)
134+
- `hm->method` - the HTTP method (GET, POST, etc.)
135135
136136
You can use these to create multiple endpoints. You need at least 3 GET endpoints and 1 POST endpoint to submit your project.
137137

src/layouts/DocsLayout.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const guides = await getCollection("guides");
1414
const grouped = guides.reduce(
1515
(acc, guide) => {
1616
const parts = guide.id.split("/");
17-
const category = parts.length > 1 ? parts[0] : "general";
17+
const category = parts.length > 1 ? parts[0] : "about";
1818
if (!acc[category]) acc[category] = [];
1919
acc[category].push(guide);
2020
return acc;

src/lib/airtable.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,3 +331,164 @@ export async function createSubmission(
331331
payout_transaction_id: r.fields.payout_transaction?.[0] ?? undefined,
332332
};
333333
}
334+
335+
export interface SubmissionWithProject extends SubmissionRecord {
336+
project_name: string;
337+
project_description: string;
338+
project_url?: string;
339+
repo_url?: string;
340+
image_url?: string;
341+
user_slack_id: string;
342+
created_at: string;
343+
}
344+
345+
export async function getPendingSubmissions(): Promise<
346+
SubmissionWithProject[]
347+
> {
348+
const submissions: SubmissionWithProject[] = [];
349+
const filter = encodeURIComponent(`{review_status}="submitted"`);
350+
let offset: string | undefined;
351+
352+
do {
353+
const url = new URL(
354+
`${BASE()}/submissions?filterByFormula=${filter}&sort%5B0%5D%5Bfield%5D=id&sort%5B0%5D%5Bdirection%5D=asc`,
355+
);
356+
if (offset) url.searchParams.set("offset", offset);
357+
const res = await fetch(url.toString(), { headers: HEADERS() });
358+
if (!res.ok) break;
359+
const data = await res.json();
360+
for (const r of data.records ?? []) {
361+
const projectId = r.fields.project?.[0];
362+
let project: ProjectRecord | null = null;
363+
if (projectId) {
364+
project = await getProjectById(projectId);
365+
}
366+
submissions.push({
367+
id: r.id,
368+
project_id: projectId ?? "",
369+
hours_at_submission: r.fields.hours_at_submission ?? 0,
370+
review_status: r.fields.review_status ?? "submitted",
371+
reviewer_notes: r.fields.reviewer_notes ?? "",
372+
multiplier: r.fields.multiplier ?? 1,
373+
payout: r.fields.payout ?? 0,
374+
payout_transaction_id: r.fields.payout_transaction?.[0] ?? undefined,
375+
project_name: project?.name ?? "",
376+
project_description: project?.description ?? "",
377+
project_url: project?.project_url,
378+
repo_url: project?.repo_url,
379+
image_url: project?.image_url,
380+
user_slack_id: project?.user_slack_id ?? "",
381+
created_at: r.createdTime ?? "",
382+
});
383+
}
384+
offset = data.offset;
385+
} while (offset);
386+
387+
return submissions;
388+
}
389+
390+
export async function getSubmissionById(
391+
id: string,
392+
): Promise<SubmissionWithProject | null> {
393+
const res = await fetch(`${BASE()}/submissions/${id}`, {
394+
headers: HEADERS(),
395+
});
396+
if (!res.ok) return null;
397+
const r = await res.json();
398+
const projectId = r.fields.project?.[0];
399+
let project: ProjectRecord | null = null;
400+
if (projectId) {
401+
project = await getProjectById(projectId);
402+
}
403+
return {
404+
id: r.id,
405+
project_id: projectId ?? "",
406+
hours_at_submission: r.fields.hours_at_submission ?? 0,
407+
review_status: r.fields.review_status ?? "submitted",
408+
reviewer_notes: r.fields.reviewer_notes ?? "",
409+
multiplier: r.fields.multiplier ?? 1,
410+
payout: r.fields.payout ?? 0,
411+
payout_transaction_id: r.fields.payout_transaction?.[0] ?? undefined,
412+
project_name: project?.name ?? "",
413+
project_description: project?.description ?? "",
414+
project_url: project?.project_url,
415+
repo_url: project?.repo_url,
416+
image_url: project?.image_url,
417+
user_slack_id: project?.user_slack_id ?? "",
418+
created_at: r.createdTime ?? "",
419+
};
420+
}
421+
422+
export async function updateSubmissionReview(
423+
id: string,
424+
data: {
425+
review_status: "approved" | "rejected";
426+
multiplier: number;
427+
reviewer_notes: string;
428+
buffs: string;
429+
},
430+
): Promise<boolean> {
431+
const res = await fetch(`${BASE()}/submissions/${id}`, {
432+
method: "PATCH",
433+
headers: HEADERS(),
434+
body: JSON.stringify({
435+
fields: {
436+
review_status: data.review_status,
437+
multiplier: data.multiplier,
438+
reviewer_notes: data.reviewer_notes,
439+
buffs: data.buffs,
440+
},
441+
}),
442+
});
443+
return res.ok;
444+
}
445+
446+
export async function updateProjectApprovedHours(
447+
id: string,
448+
approved_hours: number,
449+
): Promise<boolean> {
450+
const res = await fetch(`${BASE()}/projects/${id}`, {
451+
method: "PATCH",
452+
headers: HEADERS(),
453+
body: JSON.stringify({ fields: { approved_hours } }),
454+
});
455+
return res.ok;
456+
}
457+
458+
export async function createLedgerEntry(
459+
userAirtableId: string,
460+
submissionId: string,
461+
reason: string,
462+
): Promise<string | null> {
463+
const res = await fetch(`${BASE()}/ledger`, {
464+
method: "POST",
465+
headers: HEADERS(),
466+
body: JSON.stringify({
467+
records: [
468+
{
469+
fields: {
470+
user: [userAirtableId],
471+
type: "project_payout",
472+
reason,
473+
payout_submission: [submissionId],
474+
},
475+
},
476+
],
477+
}),
478+
});
479+
if (!res.ok) return null;
480+
const data = await res.json();
481+
return data.records?.[0]?.id ?? null;
482+
}
483+
484+
export async function linkSubmissionPayoutTransaction(
485+
submissionId: string,
486+
ledgerRecordId: string,
487+
): Promise<boolean> {
488+
const res = await fetch(`${BASE()}/submissions/${submissionId}`, {
489+
method: "PATCH",
490+
headers: HEADERS(),
491+
body: JSON.stringify({ fields: { payout_transaction: [ledgerRecordId] } }),
492+
});
493+
return res.ok;
494+
}

0 commit comments

Comments
 (0)