원클릭으로
add-endpoint
Scaffold a new Express API endpoint with controller, route (OpenAPI annotations), response type, and barrel exports.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Scaffold a new Express API endpoint with controller, route (OpenAPI annotations), response type, and barrel exports.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Low-level Cardano utilities with @meshsdk/core-cst
Cardano transaction building with @meshsdk/transaction
Cardano wallet integration with @meshsdk/wallet
Scaffold a new cron job with in-process guard, env-configurable schedule, SyncStatus DB locking, and job registry wiring.
Scaffold a new Prisma database model with project conventions (snake_case mapping, BigInt for lovelace, timestamps, relations).
Analyze feedback and evolve skills through structured improvement. The meta-skill that makes other skills better.
Scaffold a complete Express.js API endpoint following the project's established patterns: controller, route with @openapi annotations, response type, and barrel exports.
$0 - Domain/feature area (e.g., drep, overview, proposal, or a new domain like spo)$1 - HTTP method: get or post (default: get)$2 - URL path suffix (e.g., stats, :id/votes, trigger-cleanup)$3 - Short description of the endpoint (e.g., "Get aggregate SPO statistics")Look for existing files:
src/controllers/{$0}/index.ts
src/routes/{$0}.route.ts
If the domain is new, you'll also need Steps 6 and 7. If it exists, skip those steps.
Create src/controllers/{$0}/{handlerName}.ts:
import { Request, Response } from "express";
import { prisma } from "../../services";
/**
* {$1 uppercase} /{$0}/{$2}
* {$3}
*/
export const {handlerName} = async (req: Request, res: Response) => {
try {
// TODO: Implement endpoint logic
const data = {};
res.json(data);
} catch (error) {
console.error("Error in {handlerName}", error);
res.status(500).json({
error: "Failed to {$3 lowercase}",
message: error instanceof Error ? error.message : "Unknown error",
});
}
};
Handler naming conventions:
get{Resource} (e.g., getSPOStats, getDRepVotes)post{Action} (e.g., postTriggerSync, postIngestProposal)For paginated endpoints, add this query param parsing at the top:
const page = Math.max(1, parseInt(req.query.page as string) || 1);
const pageSize = Math.min(100, Math.max(1, parseInt(req.query.pageSize as string) || 20));
const skip = (page - 1) * pageSize;
For BigInt fields, always convert before JSON response:
// BigInt → string for serialization
const votingPowerStr = drep.votingPower.toString();
// BigInt → ADA string
function lovelaceToAda(lovelace: bigint): string {
return (Number(lovelace) / 1_000_000).toFixed(6);
}
Add to src/controllers/{$0}/index.ts:
export * from "./{handlerFileName}";
Add to src/routes/{$0}.route.ts:
/**
* @openapi
* /{$0}/{$2}:
* {$1}:
* summary: {$3}
* description: {Longer description}
* tags:
* - {Domain Tag}
* parameters:
* - name: paramName
* in: path|query
* required: true|false
* description: Parameter description
* schema:
* type: string|integer
* responses:
* 200:
* description: Success description
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/{ResponseType}'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
router.{$1}("/{$2}", {$0}Controller.{handlerName});
Add to src/responses/{$0}.response.ts (create if new domain):
/**
* Response type for {$3}
*/
export interface {ResponseTypeName} {
// Define fields here
}
Then export from src/responses/index.ts:
export * from "./{$0}.response";
Create src/routes/{$0}.route.ts:
import express from "express";
import { {$0}Controller } from "../controllers";
const router = express.Router();
// ... routes go here ...
export default router;
Add to src/index.ts:
import {$0}Router from "./routes/{$0}.route";
// In the middleware section:
app.use("/{$0}", apiKeyAuth, {$0}Router);
And add the controller barrel export to src/controllers/index.ts:
export * as {$0}Controller from "./{$0}";
index.ts)@openapi JSDoc annotationsrc/responses/{ page, pageSize, totalItems, totalPages }src/index.ts, controller barrel exportedWhen an endpoint needs counts from a related model (e.g., vote count per DRep), Prisma doesn't support filtered _count in findMany. Use groupBy + in-memory join:
// 1. Fetch main entities
const dreps = await prisma.drep.findMany({ ... });
const drepIds = dreps.map((d) => d.drepId);
// 2. Fetch counts via groupBy
const voteCounts = await prisma.onchainVote.groupBy({
by: ["drepId"],
where: { drepId: { in: drepIds }, voterType: VoterType.DREP },
_count: { id: true },
});
// 3. Join in memory
const voteCountMap = new Map<string, number>();
for (const vc of voteCounts) {
if (vc.drepId) voteCountMap.set(vc.drepId, vc._count.id);
}
// 4. Use in response mapping
const summaries = dreps.map((d) => ({
...d,
totalVotesCast: voteCountMap.get(d.drepId) || 0,
}));
DReps with doNotList: true should be excluded. Since the field is nullable, always use:
const whereClause = {
OR: [{ doNotList: false }, { doNotList: null }],
};
When sorting by a field not in the DB (e.g., totalVotes):
if (sortBy === "totalVotes") {
results.sort((a, b) => {
const diff = a.totalVotesCast - b.totalVotesCast;
return sortOrder === "asc" ? diff : -diff;
});
}
Note: pagination still works correctly for DB-column sorts (votingPower, name) but for computed-field sorts the page boundary may shift.
const aggregateResult = await prisma.drep.aggregate({
where: { OR: [{ doNotList: false }, { doNotList: null }] },
_sum: { votingPower: true, delegatorCount: true },
});
const total = aggregateResult._sum.votingPower ?? BigInt(0);
When computing percentages with BigInt (e.g., vote power ratios), use scaled arithmetic to preserve precision:
// Multiply by 10000 first (for 2 decimal places), then divide
const turnoutPct = totalPower > 0n
? Number((activePower * 10000n) / totalPower) / 100
: null;
For voters who can change their vote (e.g., CC members), always order by timestamp and dedupe:
const votes = await prisma.onchainVote.findMany({
where: { voterType: VoterType.CC },
orderBy: [{ votedAt: "desc" }, { createdAt: "desc" }],
});
const seenVotes = new Set<string>();
for (const vote of votes) {
const key = `${vote.ccId}-${vote.proposalId}`;
if (!seenVotes.has(key)) {
seenVotes.add(key);
// Process this vote (it's the latest)
}
}
For wall-clock calculations from epoch numbers, build a lookup map:
const epochTimestamps = await prisma.epochTotals.findMany({
where: { epoch: { in: Array.from(epochs) } },
select: { epoch: true, startTime: true, endTime: true },
});
const epochTimeMap = new Map<number, Date>();
for (const et of epochTimestamps) {
if (et.startTime && et.endTime) {
const midpoint = new Date((et.startTime.getTime() + et.endTime.getTime()) / 2);
epochTimeMap.set(et.epoch, midpoint);
}
}
Gini coefficient for decentralization (0 = equal, 1 = concentrated):
// Sort values ascending, compute weighted sum
const sorted = [...values].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
let sum = 0n, weightedSum = 0n;
for (let i = 0; i < sorted.length; i++) {
sum += sorted[i];
weightedSum += BigInt(i + 1) * sorted[i];
}
const gini = Number((2n * weightedSum - BigInt(n + 1) * sum) * 10000n / (BigInt(n) * sum)) / 10000;
HHI (Herfindahl-Hirschman Index) for concentration (0-10000):
let hhi = 0;
for (const [, data] of groupPower) {
const sharePct = Number((data.power * 10000n) / totalPower) / 100;
hhi += sharePct * sharePct;
}
Contention score (0-100, higher = more contentious):
const diff = Math.abs(yesPct - noPct);
const contentionScore = 100 - diff; // 50/50 = 100, 100/0 = 0
const isContentious = diff < 20; // Within 40-60 range
npm run build to verify TypeScript compilesnpm run swagger:generate to update API docscurl http://localhost:3000/{$0}/{$2}