create-stellar-service 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ # {{name}}: the FastAPI app and the Provider Kit gateway in one container. The app listens on 127.0.0.1:8000, inside
2
+ # the container only, and the gateway on port 8787, in front of it.
3
+ #
4
+ # docker build -t {{package}} .
5
+ # docker run -p 8787:8787 -v {{package}}-jobs:/data -e PAY_TO=G... -e PUBLIC_ORIGIN=https://your.domain {{package}}
6
+ #
7
+ # Paid jobs are kept in /data/jobs.sqlite: give /data a persistent volume. The image holds no Stellar secret key.
8
+ # fly.toml deploys it on Fly.io: npm run launch does, or npm run deploy.
9
+
10
+ FROM node:24-slim
11
+ RUN apt-get update && apt-get install -y --no-install-recommends python3 python3-venv && rm -rf /var/lib/apt/lists/*
12
+ ENV NODE_ENV=production JOB_DB_PATH=/data/jobs.sqlite PATH=/opt/venv/bin:$PATH
13
+ WORKDIR /app
14
+ COPY requirements.txt ./
15
+ RUN python3 -m venv /opt/venv && pip install --no-cache-dir -r requirements.txt
16
+ COPY package.json package-lock.json ./
17
+ {{vendor}}
18
+ RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
19
+ COPY app.py provider.config.json start.sh ./
20
+ RUN mkdir -p /data && chown node:node /data
21
+ VOLUME /data
22
+ EXPOSE 8787
23
+ HEALTHCHECK --interval=30s --timeout=5s CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || 8787) + '/healthz').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
24
+ # A host's new volume may belong to root, as Fly.io's do: started as root, the container only gives /data to the node
25
+ # user, then runs the service as node. Started as another user, it runs as that user.
26
+ ENTRYPOINT ["sh", "-c", "if [ \"$(id -u)\" = 0 ]; then chown node:node /data 2>/dev/null || true; exec setpriv --reuid=node --regid=node --init-groups -- \"$@\"; fi; exec \"$@\"", "sh"]
27
+ CMD ["sh", "start.sh"]
@@ -0,0 +1,39 @@
1
+ """{{name}}: the service itself, a FastAPI app that knows nothing about payments.
2
+
3
+ The Provider Kit gateway (provider.config.json) stands in front of it. It publishes the manifest, takes the USDC
4
+ payment, queues jobs and keeps them for recovery, and POSTs only validated input here. Keep the models below in step
5
+ with the schemas in provider.config.json: agents read those before they pay.
6
+
7
+ `npm run dev` starts it next to the gateway: {{apiCommand}}
8
+ """
9
+
10
+ import re
11
+
12
+ from fastapi import FastAPI
13
+ from pydantic import BaseModel, Field
14
+
15
+ app = FastAPI(title={{nameJson}})
16
+
17
+
18
+ class SummarizeIn(BaseModel):
19
+ text: str = Field(min_length=1, max_length=20000, description="The text to summarize")
20
+ sentences: int = Field(default=1, ge=1, le=5, description="How many sentences to keep")
21
+
22
+
23
+ class SummarizeOut(BaseModel):
24
+ summary: str
25
+ sentences: int = Field(description="Sentences in the text")
26
+ words: int = Field(description="Words in the text")
27
+
28
+
29
+ @app.post(
30
+ "/summarize",
31
+ summary="Summarize",
32
+ description="Returns the first sentences of an English text, with its sentence and word counts. Use it to shorten "
33
+ "text before passing it on. Accepts up to 20,000 characters; it does not paraphrase.",
34
+ )
35
+ def summarize(body: SummarizeIn) -> SummarizeOut:
36
+ # Your own work goes here, such as a model call. The gateway sends the job id in the Idempotency-Key header, so
37
+ # a job it runs again after a restart can be recognized.
38
+ parts = [part.strip() for part in re.split(r"(?<=[.!?])\s+", body.text) if part.strip()]
39
+ return SummarizeOut(summary=" ".join(parts[: body.sentences]), sentences=len(parts), words=len(body.text.split()))
@@ -0,0 +1,2 @@
1
+ fastapi==0.115.12
2
+ uvicorn==0.34.2
@@ -0,0 +1,16 @@
1
+ #!/bin/sh
2
+ # Starts the FastAPI app and the gateway in front of it. When either one stops, so does the other, and the container
3
+ # exits for its host to restart. A stop signal reaches both: the gateway finishes the requests in flight.
4
+
5
+ uvicorn app:app --host 127.0.0.1 --port 8000 &
6
+ api=$!
7
+ node node_modules/@stellar-agent-services/provider-kit/dist/cli/index.js serve --config provider.config.json &
8
+ gateway=$!
9
+
10
+ asked=0
11
+ trap 'asked=1; kill -TERM "$api" "$gateway" 2>/dev/null' TERM INT
12
+ while kill -0 "$api" 2>/dev/null && kill -0 "$gateway" 2>/dev/null; do sleep 1; done
13
+ kill -TERM "$api" "$gateway" 2>/dev/null
14
+ wait
15
+ # 0 when asked to stop; 1 when one of them stopped by itself.
16
+ [ "$asked" = 1 ]
@@ -0,0 +1,31 @@
1
+ # {{name}}: the service and its job database, on any host that runs a container.
2
+ #
3
+ # docker build -t {{package}} .
4
+ # docker run -p 8787:8787 -v {{package}}-jobs:/data -e PAY_TO=G... -e PUBLIC_ORIGIN=https://your.domain {{package}}
5
+ #
6
+ # Paid jobs are kept in /data/jobs.sqlite: give /data a persistent volume. The image holds no Stellar secret key.
7
+ # fly.toml deploys it on Fly.io: npm run launch does, or npm run deploy.
8
+
9
+ FROM node:24-slim AS build
10
+ WORKDIR /app
11
+ COPY package.json package-lock.json tsconfig.json ./
12
+ {{vendor}}
13
+ RUN npm ci --ignore-scripts
14
+ COPY src ./src
15
+ RUN npx tsc
16
+
17
+ FROM node:24-slim
18
+ ENV NODE_ENV=production JOB_DB_PATH=/data/jobs.sqlite
19
+ WORKDIR /app
20
+ COPY package.json package-lock.json ./
21
+ {{vendor}}
22
+ RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
23
+ COPY --from=build /app/dist ./dist
24
+ RUN mkdir -p /data && chown node:node /data
25
+ VOLUME /data
26
+ EXPOSE 8787
27
+ HEALTHCHECK --interval=30s --timeout=5s CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || 8787) + '/healthz').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
28
+ # A host's new volume may belong to root, as Fly.io's do: started as root, the container only gives /data to the node
29
+ # user, then runs the service as node. Started as another user, it runs as that user.
30
+ ENTRYPOINT ["sh", "-c", "if [ \"$(id -u)\" = 0 ]; then chown node:node /data 2>/dev/null || true; exec setpriv --reuid=node --regid=node --init-groups -- \"$@\"; fi; exec \"$@\"", "sh"]
31
+ CMD ["node", "--disable-warning=ExperimentalWarning", "dist/server.js"]
@@ -0,0 +1,57 @@
1
+ // {{name}}: a paid service on Stellar, built with the Provider Kit (Provider Standard v1).
2
+ //
3
+ // Settings come from .env: public values, and on mainnet the facilitator's API key. This service never needs a Stellar
4
+ // secret key. Paid jobs are kept in SQLite (JOB_DB_PATH), so they survive a restart, and a client that lost its
5
+ // response can still collect the result. Add services in src/services/ and list them below. `npm run launch` starts
6
+ // this file with PUBLIC_ORIGIN set to the public address, to read the manifest it will serve there.
7
+
8
+ import { createProvider, httpFacilitator, listen, sqliteStore, type ProviderConfig } from "@stellar-agent-services/provider-kit";
9
+ import { summarize } from "./services/summarize.js";
10
+
11
+ const port = Number(process.env["PORT"] ?? "8787");
12
+ const origin = process.env["PUBLIC_ORIGIN"] ?? `http://localhost:${port}`;
13
+ const dev = origin.startsWith("http://localhost");
14
+ // stellar:testnet takes test money; stellar:pubnet is mainnet, real USDC.
15
+ const network = (process.env["STELLAR_NETWORK"] ?? "stellar:testnet") as ProviderConfig["network"];
16
+ const payTo = process.env["PAY_TO"];
17
+ if (!payTo) throw new Error(`PAY_TO is empty: npm run launch sets up your own wallet${network === "stellar:testnet" ? ", and npm run dev makes a throwaway testnet account meanwhile" : ""}.`);
18
+ const agentId = process.env["AGENT_ID"];
19
+ const rpcUrl = process.env["SOROBAN_RPC_URL"];
20
+
21
+ // The facilitator verifies and settles payments: x402.org's on testnet, OpenZeppelin's on mainnet, which wants an API
22
+ // key. FACILITATOR_AUTH is sent as the Authorization header: "Bearer <key>".
23
+ const facilitatorUrl = process.env["FACILITATOR_URL"] ?? (network === "stellar:pubnet" ? "https://channels.openzeppelin.com/x402" : "https://x402.org/facilitator");
24
+ const facilitatorAuth = process.env["FACILITATOR_AUTH"];
25
+ if (network === "stellar:pubnet" && !facilitatorAuth && !process.env["FACILITATOR_URL"]) {
26
+ throw new Error('FACILITATOR_AUTH is empty: get an API key at https://channels.openzeppelin.com/gen, and set FACILITATOR_AUTH="Bearer <key>" in .env.');
27
+ }
28
+
29
+ const provider = createProvider({
30
+ baseUrl: origin,
31
+ dev,
32
+ network,
33
+ payTo,
34
+ facilitator: httpFacilitator(facilitatorUrl, { dev, ...(facilitatorAuth ? { headers: { Authorization: facilitatorAuth } } : {}) }),
35
+ // Search matches only this name and description: use the words buyers type.
36
+ provider: {
37
+ name: {{nameJson}},
38
+ description: {{descriptionJson}},
39
+ dataUse: { storesInputs: false, trainsOnInputs: false },
40
+ ...(agentId ? { agentId: Number(agentId) } : {}),
41
+ },
42
+ services: [summarize],
43
+ store: sqliteStore(process.env["JOB_DB_PATH"] ?? "./jobs.sqlite"),
44
+ // Decides payment_uncertain jobs from the chain. Testnet defaults to the public RPC; mainnet needs one.
45
+ ...(rpcUrl ? { rpc: rpcUrl } : {}),
46
+ });
47
+
48
+ // Fails fast if the facilitator can't settle this network, and resumes jobs a restart left behind.
49
+ await provider.ready();
50
+ const server = await listen(provider, { port });
51
+ console.log(`Listening on ${origin}`);
52
+ console.log(`Manifest: ${provider.manifestUrl}`);
53
+ console.log(`payTo: ${payTo}`);
54
+
55
+ for (const signal of ["SIGINT", "SIGTERM"] as const) {
56
+ process.once(signal, () => void server.close().then(() => process.exit(0)));
57
+ }
@@ -0,0 +1,47 @@
1
+ // summarize: {{price}} USDC, charged only if it succeeds. Replace the handler with your own work, such as a model call,
2
+ // and keep the description, schemas and example in step with it: agents read them before they pay.
3
+
4
+ import { defineService } from "@stellar-agent-services/provider-kit";
5
+
6
+ type Input = { text: string; sentences?: number };
7
+
8
+ export const summarize = defineService({
9
+ name: "summarize",
10
+ title: "Summarize",
11
+ description:
12
+ "Returns the first sentences of an English text, with its sentence and word counts. Use it to shorten text before passing it on. Accepts up to 20,000 characters; it does not paraphrase.",
13
+ price: "{{price}}",
14
+ charge: "on-success",
15
+ expectedDurationSeconds: 1,
16
+ inputSchema: {
17
+ type: "object",
18
+ required: ["text"],
19
+ properties: {
20
+ text: { type: "string", minLength: 1, maxLength: 20000, description: "The text to summarize" },
21
+ sentences: { type: "integer", minimum: 1, maximum: 5, default: 1, description: "How many sentences to keep" },
22
+ },
23
+ additionalProperties: false,
24
+ },
25
+ outputSchema: {
26
+ type: "object",
27
+ required: ["summary", "sentences", "words"],
28
+ properties: {
29
+ summary: { type: "string" },
30
+ sentences: { type: "integer", description: "Sentences in the text" },
31
+ words: { type: "integer", description: "Words in the text" },
32
+ },
33
+ },
34
+ examples: [
35
+ {
36
+ input: { text: "Stellar settles in seconds. Fees are tiny.", sentences: 1 },
37
+ output: { summary: "Stellar settles in seconds.", sentences: 2, words: 7 },
38
+ },
39
+ ],
40
+ handler: ({ text, sentences = 1 }: Input) => {
41
+ const parts = text
42
+ .split(/(?<=[.!?])\s+/)
43
+ .map((part) => part.trim())
44
+ .filter(Boolean);
45
+ return { summary: parts.slice(0, sentences).join(" "), sentences: parts.length, words: text.split(/\s+/).filter(Boolean).length };
46
+ },
47
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "lib": ["ES2023"],
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "types": ["node"],
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "rootDir": "src",
11
+ "outDir": "dist"
12
+ },
13
+ "include": ["src"]
14
+ }