SKILL.md
Two concepts: PUBLISH vs LIST — never confuse them
This skill handles two fundamentally different concepts. Mixing them up is the #1 source of wrong answers.
| Concept | What it means | Functions |
|---|---|---|
| PUBLISH (发布) | Make something accessible — a URL works, or code is on GitHub | publishpreview, unpublishpreview, listpublishedpreviews, opensource, removeopensource, listopensource, getopensource, fork, validateopen_source |
| LIST (上架) | Make something discoverable/purchasable on the marketplace | Free: listindashboard, unlistfromdashboard, deletelisting, getlistingstatus<br>Paid: createpaidservice, submitforreview, getreviewstatus, publishservice, unpublishservice, listmyservices, getservice, updateservice, deleteservice, restoreservice<br>Cover: uploadcoverimage<br>Browse + consumer: exploreservices, getservicedetail, getservicepricing, getservicereviews, writeservicereview, favoriteservice, unfavoriteservice, getfavoriteservices, getuserservices, getserviceearnings, getearningssummary, getservicetags, getfeaturedservices<br>Projects query: exploreprojects, myprojects, favoriteprojects, gettabcounts, getpopulartags, getuserprojects, favoriteproject, unfavorite_project |
Publishing does NOT auto-list. publishpreview() only allocates the URL. opensource() only pushes code. Neither makes the project discoverable on the marketplace — that requires a separate, deliberate LIST call.
Listing has two flows
| Flow | When to use | Review? | Pricing? | Functions |
|---|---|---|---|---|
| Free listing | Free project, show on /projects gallery |
No | No | listindashboard() |
| Paid listing | Charge for access via x402 | Required (6-check review, must pass before publishing) | Yes (USDC/USDG/USDC(Solana) on platform networks — default Base+Monad+Robinhood+X Layer+Solana, follows all) |
createpaidservice() → submitforreview() (required) → publish_service() |
POST /api/servicesno longer acceptsservicetype: "freeproject". Free listing is done bylistindashboard()(the project gallery flow). Paid listing usescreatepaidservice()+ review + publish (the service API flow).
Limited-time free promo ≠ this skill
After a paid service is listed, the owner may run a time-window free promotion (freepromostart / freepromoend). That is not marketplace listing work and is not implemented here.
| Concept | What it is | Where |
|---|---|---|
| Free listing | Free project on /projects gallery |
this skill → listindashboard() |
freetrialcount |
N free calls before charge (payperuse only) | this skill → createpaidservice(..., freetrialcount=N) |
| Limited-time free promo | Calendar window: amount-0 verify, no settle/debit | x402 skill → skills/x402/references/selling.md section Limited-time free promotion |
If the user asks to “开限时免费 / free promo / free for N days” on an already-paid listing: read the x402 skill (self-check P1–P5, then PUT free-promo). Do not invent APIs in community-publish or confuse it with freetrialcount.
Visibility model — read this before answering "can others see it?"
A project's "publicness" is three orthogonal switches, not one:
| Switch | Off state | On state | Flipped by |
|---|---|---|---|
| URL access | Visiting the URL returns 404 | URL works for anyone who has the link | publishpreview / unpublishpreview |
| Gallery discoverability | Not on /projects gallery |
Appears in the gallery | listindashboard / unlistfromdashboard |
| Marketplace listing | Not on the Service Marketplace | Discoverable + purchasable | createpaidservice + publishservice / unpublishservice |
A project can be in any combination. Never collapse these into "is it public yet".
Status questions are read-only operations. Whenever the user asks:
- "is it visible / public / discoverable yet?"
- "上架了吗 / 在 dashboard 上吗 / 别人能看到吗"
- "is the listing live?"
The authoritative answer comes ONLY from a fresh getlistingstatus(slug) (free) or getreviewstatus(service_id) (paid) call. Do NOT infer from past actions.
Project types — three only
| type | What it is | Eligible for publish_preview()? |
|---|---|---|
task |
Scheduled cron/interval job | No (no HTTP port) |
service |
Long-running HTTP service (dashboard, API, page) | Yes |
script |
One-shot script | No (no HTTP port) |
Routing — match user intent to the right action
A. Status intents — user wants to know current state
| Sample phrasing | Action |
|---|---|
| "is it visible / public / discoverable / live?" | getlistingstatus(slug) |
| "上架了吗 / 在 dashboard 上吗 / 别人能不能看到" | getlistingstatus(slug) |
| "what URLs do I have published?" / "我发布了哪些" | listpublishedpreviews() |
| "what's open-sourced?" / "都有哪些开源代码" | listopensource(...) |
| "我的服务" / "my services" / "我的付费服务" | listmyservices() |
| "审核状态" / "审核通过了吗" / "review status" | getreviewstatus(service_id) |
B. Action intents — user wants to change state
| Sample phrasing | Action | Notes |
|---|---|---|
| "publish" / "share" / "make public" / "公开" / "发布" (no qualifier) | publishpreview(previewid) |
Allocates the URL only. Listing is NOT auto-flipped. |
| "list on the dashboard" / "上架" / "show on community" / "make discoverable" / "发到广场" | listindashboard(slug) |
Free listing. Requires the preview to already exist. |
| "上架付费服务" / "make this a paid service" / "上架到服务市场(付费)" | createpaidservice(...) → submitforreview() (recommended) → publish_service() |
Paid listing. Needs x402 config first. |
| "publish AND list" / "发布并上架" | publishpreview() THEN listin_dashboard() |
Two separate calls in order. |
| "remove from dashboard" / "下架" / "unlist" / "hide from gallery" | unlistfromdashboard(slug) |
Free listing only. Soft-unlist (sets ispublic=false, reviewstatus='unlisted', preserves stats). Preview URL stays alive. |
| "下架付费服务" / "unpublish service" | unpublishservice(serviceid) |
Paid listing only. |
| "open source" / "open-source the code" / "开源代码" | opensource(projectdir) |
Pushes code to GitHub. Does NOT list. |
| "unpublish the URL" / "take down the link" / "停止服务" | unpublish_preview(slug) |
Stops the preview container service only. Does NOT affect listing state (ispublic/reviewstatus unchanged). URL becomes inaccessible (404). |
| "remove the open source" / "delete from GitHub" | removeopensource(slug) |
|
| "fork" / "install someone's project" | fork(source) |
|
| "提交审核" / "submit for review" | submitforreview(service_id) |
Paid only. Required — must pass before publishing |
| "发布服务" / "publish my service" | publishservice(serviceid) |
Paid only, requires approved or unlisted state |
| "更新服务" / "update service" | updateservice(serviceid, ...) |
Paid only |
| "删除服务" / "delete service" | deleteservice(serviceid) |
Paid only |
| "删除项目" / "delete listing" / "permanently remove from marketplace" | delete_listing(slug) |
Free listing only. Permanently deletes the listing row AND the communityslugs record. URL becomes inaccessible (404). Removes from both explore and my-projects. Use unlistfrom_dashboard() to hide without deleting. |
| Ambiguous after rereading | Ask one question | "你是要 (a) 发布公开 URL,(b) 免费上架到广场,(c) 付费上架到服务市场,还是 (d) 开源代码?" |
Cross-link via publisher: binding
When the same project has BOTH a public URL AND open-sourced code, you want them paired so the frontend renders "View Source" on the listing card and "Visit Live Demo" on the code card. This skill drives that pairing through one explicit binding in project.yaml.
How to declare the binding
Add a publisher: block to project.yaml:
name: my-app
type: service
version: 1.0.0
publisher:
code_slug: my-app # OPTIONAL — defaults to manifest.name
public_slug: my-app-pub # OPTIONAL — URL suffix; defaults to code_slug
Both fields are optional. If omitted, both default to manifest.name.
Either side can be published first
The gateway holds a pending entry until the second side arrives. No ordering requirement, no manual link step.
| Order | What happens |
|---|---|
opensource first → publishpreview second |
opensource records pending entry; publishpreview consumes it and links |
publishpreview first → opensource second |
publishpreview records pending entry (needs publishercodeslug arg); opensource consumes it and links |
Manual repair (rare)
If a pairing was wired wrong (e.g. after a rename), use:
link_to_listing(listing_slug="2004-my-app-pub", code_slug="my-app")
Architecture
community.iamstarchild.com (single gateway domain)
│
┌─────────────────┼─────────────────────┐
│ │ │
┌────────▼─────────┐ ┌───▼────────────┐ ┌─────▼──────────┐
│ /api/register │ │/api/code- │ │ /api/services │
│ /api/unregister │ │ projects/* │ │ /api/projects- │
│ /api/list │ │ (GitHub-backed)│ │ query/* │
└────────┬─────────┘ └───┬────────────┘ └─────┬──────────┘
│ │ │
┌────────▼─────────┐ ┌───▼────────────┐ ┌─────▼──────────┐
│ DB: route table │ │ GitHub: │ │ DB: │
│ + project_ │ │ community- │ │ service_ │
│ listings │ │ projects repo │ │ listings │
└──────────────────┘ └────────────────┘ │ (paid services)│
publish_preview() open_source() └────────────────┘
list_in_dashboard()
create_paid_service()
PUBLISH: publish_preview() — public URL
publishpreview(previewid, slug="", title="", publishercodeslug="")
Map a running service to https://community.iamstarchild.com/{user_id}-{slug}.
preview_id: frompreview(action='serve'). Must bestatus=running.slug: URL suffix only (lowercase alphanumeric + hyphens, 3-50 chars). User_id prefix is added automatically.title: display name for the listing.publishercodeslug: optional cross-link binding to a code project's slug.
Returns {"ok": True, "url": "...", "publisher": {...}, "hint": "...", "x402detected": bool} — plus a nextstep warning when x402_detected is true (complete the paid-listing chain).
Constraints:
publish_previewdoes NOT create a paid listing. If the endpoint
charges via x402 (returns 402), the publish flow is INCOMPLETE until you also run createpaidservice → submitforreview (recommended) → publishservice — otherwise the marketplace shows nothing or "free". The return value flags this (x402detected: true + next_step) when billing is detected.
- Max 20 published previews per user (gateway returns 429 over).
- Service must be running. Stops working when the container goes down.
- Only works inside the Starchild Fly container (needs
FLYMACHINEID). - Listing visibility default is
ispublic=false. A successfulpublishpreviewallocates the URL but does NOT make it discoverable. Discovery requires a separatelistindashboard()call.
Companions:
unpublishpreview(slug)— stop the preview container service. URL becomes inaccessible (404). Does NOT affect listing state (ispublic/review_statusunchanged).listpublishedpreviews()— all currently published preview URLs for this user.
PUBLISH: open_source() — push code to GitHub
opensource(projectdir, version_bump="patch", message="")
Push project source to community-projects/projects/{user_id}/{slug}/ on GitHub.
project_dir: e.g.output/projects/my-taskversion_bump:patch|minor|major|nonemessage: commit message body describing what this version changed.
You (the agent) should always compose this based on the actual code changes you made in this session — never leave it blank if you know what changed. Aim for one to three short lines describing the user-visible change.
This is a PUBLISH action only — it does NOT list anything on the marketplace. To make a project discoverable, call listindashboard() (free) or createpaidservice() (paid) separately after publishing.
Companions:
fork(source, dest_dir=None)— install someone else's open-sourced project locallylistopensource(type=None, tag=None, user=None, q=None)— browse the GitHub cataloggetopensource(source)— fetch one project's full metadataremoveopensource(slug)— delete project directory from GitHub catalog (owner only)validateopensource(project_dir)— pre-flight check before publishing
Project structure
Every project under output/projects/{slug}/:
project.yaml # metadata (name, version, type, env_required, sc_proxy, publisher)
PROJECT.md # required sections: What / Required env / How to start / Outputs / Troubleshooting
.env.example # all env vars with placeholder values
.gitignore # secrets blacklist
src/
├── run.py # for type=task (must start: # -*- task-system: v3 -*-)
├── index.html # for type=service (or app.py + frontend)
└── main.py # for type=script
LIST (FREE): listindashboard() — show on /projects gallery
listindashboard(slug, name=None, description="", cover_url=None, tags=None)
Make a published preview discoverable in the public gallery at https://community.iamstarchild.com/projects. Without this, the preview URL works but is invisible to anyone who doesn't already know it.
slug: the full slug returned bypublishpreview()(i.e.{userid}-{suffix}).name: gallery card display name. Defaults toslug.description: ≤500 chars.coverurl: must be onstorage.googleapis.com,image.thum.io, orapi.microlink.io. To upload a user-provided image, calluploadcoverimage(slug, filepath)first — it handles presign → GCS upload → returns the public URL. See [Cover Image Upload](#cover-image-upload-flow) below.tags: ≤5 tags, ≤20 chars each.
Returns {"ok": True, "listing": {...}, "url": "...", "dashboard_url": "..."}.
Constraints:
- Requires
publish_preview()to have run first for the same slug — returns 404 otherwise. - Idempotent: calling again with different name/tags updates the existing listing.
- No review, no pricing — this is the free listing flow.
Companions:
unlistfromdashboard(slug)— soft-unlist from gallery (setsispublic=false,reviewstatus='unlisted', preserves view/favorite counts). URL stays alive. To re-list, calllistindashboard()again.deletelisting(slug)— permanently delete the listing row AND thecommunityslugsrecord (removes view/favorite counts). URL becomes inaccessible (404). Removes from both explore and my-projects. Useunlistfromdashboard()to hide without deleting.getlistingstatus(slug)— read-only check: returns{ok, exists, is_public, listing}.
LIST (PAID): Paid service listing on the Service Marketplace
Paid services charge for access via x402 (on-chain USDC/USDG settlement on the platform's enabled networks — by default Base + Monad + Robinhood + X Layer + Solana, following the all mode). An automated 6-check review is required before publishing — the service must pass all checks (approved) before publish_service() will work. API call examples are optional but recommended.
Multi-chain payment networks (plans-280)
Every paid service has a networks_mode that decides which chains buyers can pay on:
networks_mode |
Behavior | When to use |
|---|---|---|
"all" (default) |
Accept payment on all platform mainnets (currently Base + Monad + Robinhood + X Layer + Solana; new chains are picked up automatically with no code change). The gateway stores supported_networks as NULL and expands it at read time. |
The common case — pass nothing or networks_mode="all". |
"custom" |
Accept payment only on the chains listed in supported_networks (a non-empty list of CAIP-2 ids, e.g. ["eip155:8453"]). Does NOT follow platform expansion. |
The user explicitly says "only Base" / "only Monad" / a specific subset. |
Rules:
- Default is
all. Never hard-code a single chain like['eip155:8453']as the default — that re-introduces the old Base-only behavior. customrequires a non-emptysupported_networks; an empty list is rejected.provider_walletis an EVM address used on every enabled chain (the Starchild facilitator settles to the same address on each chain). It is NOT Base-only.- Buyers see the 402
acceptsarray (one entry per enabled chain, same price) and pick one chain per payment — this is standard x402 multi-accepts, not a protocol change. - Gas for settlement is paid by the platform (Starchild facilitator), not the provider.
- To switch an existing service back to
all:updateservice(serviceid, networks_mode="all"). - To restrict to a subset:
updateservice(serviceid, networksmode="custom", supportednetworks=["eip155:8453"]).
This aligns with the x402 skill's monetize default (all). The two skills are on the same release train — if the gateway 402 accepts and the marketplace listing show different chains, one side was configured custom while the other stayed all.
Service lifecycle & review states (review is ADVISORY)
create ──▶ published ──▶ submit_for_review ─▶ pending ─▶ approved / rejected
│ (required before publishing — must pass to go live)
│ │ fix via update_service(), re-check
▼ ▼
publish_service() ─────────────────▶ listed ◀─▶ unlisted (owner takedown / re-list)
│
▼
unavailable ──▶ restore ──▶ listed
Review is a self-check, not a gate: submitforreview() runs 5 automated checks (apireachable, pricingconsistency, x402payment, responsematch, doccompleteness, examplesprovided) and stores a report for the owner. publishservice() requires the service to be in approved state (or unlisted for re-listing). The review must pass before publishing — run submitfor_review() first so a broken endpoint is caught before buyers can pay for it. A rejected report does NOT block listing; a check run against an already-listed service never delists it.
⚡ Scenario Selection Decision Tree — MUST follow before creating any paid service
Step 1: Does the service have a Starchild project page (published via publish_preview())?
- YES, and the page is free to browse → Flow D. Use
servicetype="paidproject"+projectslug. The free page is published viapublishpreview(), and the paid API sits behind x402 on/api/routes. The upstream app serves the free intro page at/and the paid API at/api/. - YES, but the entire page requires payment → Flow B (Form 1). Use
servicetype="paidproject"+project_slug. The user implements their own access control (paywall + credential validation). See the x402 skill's "Paid Project: two forms" section. - NO (standalone API, no project page) → Flow C or E. Use
servicetype="paidapi"WITHOUTproject_slug. Do NOT create an index.html or publish a preview — there is no free page. The public URL root will show the x402 402 challenge or gateway info.
Step 2: Does the user want multiple API endpoints at different prices?
- YES → Use
apiendpointsarray in ONEcreatepaid_service()call (Flow E). Do NOT create multiple separate services. - NO → Single endpoint, use
api_endpointonly.
Step 3: Combine the answers:
| User wants | Free page? | Multi-endpoint? | Flow | service_type | project_slug | api_endpoints |
|---|---|---|---|---|---|---|
| Paid subscription project (entire site behind paywall) | YES | NO | B | paid_project |
required | — |
| Standalone paid API (no webpage) | NO | NO | C | paid_api |
omit | — |
| Free intro page + paid API | YES | NO | D | paid_project |
required | — |
| Free intro page + multiple paid APIs | YES | YES | D+E | paid_project |
required | required |
| Multiple paid APIs (no webpage) | NO | YES | E | paid_api |
omit | required |
⚠️ Common Flow confusion mistakes (from real incidents)
| Mistake | What goes wrong | Correct action |
|---|---|---|
User says "write an intro page AND a paid API" but agent uses paid_api + creates a separate project preview |
Service and project are disconnected — marketplace shows two items, one free (blank) and one paid | Use paidproject + projectslug (Flow D). The intro page and API are ONE service. |
| User says "pure paid API" but agent creates an index.html and publishes a preview | Unnecessary free project page clutters the marketplace; the intro page may show blank/JSON | Do NOT create index.html or publishpreview. Use paidapi (Flow C). The x402 gateway's 402 response IS the API's self-description. |
| User says "multiple API endpoints" but agent creates N separate services | N marketplace cards instead of 1; port conflicts; upstream confusion | Create ONE service with api_endpoints array (Flow E). |
| Agent reuses an upstream port already taken by another service | Gateway proxies to the WRONG upstream — responses are from a different service | Each service MUST have a unique upstream port. Check .x402/services.json for conflicts. |
Agent creates start.py with /docs route that conflicts with upstream's /docs |
Flask AssertionError: View function mapping is overwriting an existing endpoint |
Do NOT define /, /docs, or /index.html routes in both start.py and the upstream app — define them in only one place. |
Key rules:
- Do NOT pass
projectslugfor standalone paid APIs.projectslugbelongs topaidprojectonly — including the "free webpage + paid API" pattern (Flow D, which usespaidproject). Passing a preview slug or a non-existent slug for a standalonepaid_apicreates a phantom association — the backend will silently clear it, but you should not have passed it in the first place. - Routing rule: service tied to a project page →
paidproject;paidapiis ONLY for standalone APIs with no project page. If your API has a published Starchild project (landing page/dashboard) that users can browse for free, useservicetype="paidproject"+projectslug— this merges the service into the project card in the marketplace. If there is NO free project page, usepaidapiand do NOT setprojectslug. Passingpaidapi+projectslugis auto-upgraded topaidprojectbycreatepaidservice()(with aprojectslugwarningin the response) — the final listing is alwayspaid_project. projectslugmust be the full published slug WITH user prefix (e.g.33-my-app), and must correspond to an existing row inprojectlistings(i.e.publishpreview()+listin_dashboard()must have been called first).api_endpointsis for services with multiple endpoints at different prices; each endpoint has its ownpath,price, and optionallabel.- A project with
project_slugset will NOT appear in the "Free" tab — it moves to "All" and "Paid" tabs. - Merged-into-project-card visibility: when a listed service has
projectslugpointing to a PUBLIC project, it is folded into that project's card in unified marketplace views. Consequence: the service will NOT appear as a standalone item inexploreservices()orlistmyservices()— this is by design, not a listing failure. It is still live and purchasable via the project card,getservice(serviceid), andgetuserservices(userid), and it IS discoverable viaexploremarketplace()(unified feed). To verify a merged service is listed, checkgetservice()→reviewstatus == "listed", notexplore_services()results. - When the user asks for multiple APIs, create ONE service with
api_endpoints— do NOT create multiple separate services. See Flow E.
Tagging — predefined tag slugs for marketplace filtering
When creating a paid service, pass tags with 1-3 tag slugs from the predefined list below. The agent should choose the most relevant tags based on the service's name and description. Tags are used for marketplace filtering and discovery — they replace the old category field.
Predefined tag slugs (pick 1-3 most relevant):
| Domain | Tags |
|---|---|
| DeFi & Trading | defi, trading, dex, dex-swap, lending, lending-yield, yield, staking, derivatives, bridge |
| On-chain Data | onchain-data, token-analytics, price-feed, wallet, wallet-portfolio, nft |
| AI & ML | ai-inference, llm-inference, text-analysis, image-generation, text-to-speech, video-transcription, translation |
| Web & Data | web-search, web-scraping, screenshot-pdf, news-feed, seo, data-service, data-storage, analytics |
| Security & Compliance | aml-sanctions, security, privacy, threat-detection, agent-safety, agent-trust |
| Infrastructure | smart-contract, oracle, zk-proofs, layer2, mev, compute, storage, developer-tools, identity, payment, payments |
| Social & Media | social, social-media, gaming, metaverse |
| Finance (TradFi) | stock-equity, sec-edgar, real-estate, insurance, prediction-market |
| Other | dao, governance, email-sms, weather, geolocation, healthcare, agriculture, astrology-fortune, rwa, research-academic, legal-gov, launchpad |
Example: a DeFi price API → tags=["defi", "price-feed", "trading"]
Flow B — Paid Project listing
A paid project charges for access. There are two forms — both use servicetype="paidproject" + project_slug:
Form 1: Entire page behind paywall — the page itself requires payment. The user implements their own access control (a login-like component with credential validation). The platform provides the x402 payment protocol; the user implements the paywall UI and credential logic. See the x402 skill's "Paid Project: two forms" section for implementation details and the "How to pay with Agent" documentation template.
Form 2: Free page + paid API — the page is free to browse, API calls cost money. This is Flow D (below). The upstream app serves the free intro page at / and the paid API at /api/*.
Both forms are the same pattern — the only difference is what the user implements (paywall interceptor for Form 1, nothing extra for Form 2).
- Have a running project with a public URL (via
publish_preview()). - Configure x402 charging on the project's access endpoint using the x402 skill.
The endpoint must return 402 Payment Required when unpaid, and 200 + data after payment.
- Create the service record:
create_paid_service(
name="Premium Trading Signals",
description="Real-time trading signals with on-chain confirmation.",
service_type="paid_project",
tags=["trading", "onchain-data"],
project_slug="33-premium-signals", # FULL published slug WITH user prefix (the URL path segment)
api_endpoint="https://community.iamstarchild.com/33-premium-signals",
provider_wallet="0xAbC...yourEvmWallet", # EVM address for Base/Monad/Robinhood/X Layer; Solana address auto-fetched from Privy wallet
pricing_model="monthly",
price=10,
service_description="Subscribers get a dashboard with live trading signals.",
)
Required paid-project fields: name, description, servicetype, projectslug, apiendpoint, providerwallet, pricingmodel, price, servicedescription. Recommended: tags (1-3 predefined tag slugs for marketplace filtering).
⚠️ projectslug must be the full published slug including the user prefix (e.g. 33-premium-signals, exactly the path segment in the project URL https://community.iamstarchild.com/<slug>/). The gateway derives the API endpoint as publicUrl + "/" + projectslug when apiendpoint is not set, so an unprefixed or wrong slug breaks endpoint derivation and the project↔service association. Fix an existing record with updateservice(serviceid, projectslug="<full-slug>") — no re-listing needed.
- Required: run the automated review — paid services must pass review
before they can be published. A broken endpoint listed on the marketplace can take buyers' money before you notice:
submit_for_review(service_id) # kicks off 6 automated checks asynchronously
get_review_status(service_id) # poll until no longer pending, then show the
# report to the user — THEY decide what to fix
A rejected report blocks publishing. Read reviewfeedback + latesttask.checks, fix with updateservice(), and re-run submitfor_review() until approved.
- Publish once the review passes (approved):
publish_service(service_id)
The check can also be run again later against a listed service — it never delists it.
Flow C — Paid API listing
A paid API is an external API service that already implements x402 charging.
⚠️ Do NOT pass
project_slugfor standalone paid APIs.projectslugis ONLY forpaidproject(required) or the "free webpage + paid API"
pattern (Flow D, where a published Starchild project page exists). For a standalonepaidapiwith no associated free project page, omitprojectslugentirely.
The backend validatesprojectslugagainstprojectlistingsand silently clears
non-existent slugs, but you should not pass it in the first place.
⚠️ Choosepaid_projectif the API belongs to a published Starchild project.
If your API has a landing page / dashboard published viapublish_preview()(i.e. it
exists as a project on community.iamstarchild.com), useservicetype="paidproject"
+projectslug=<full published slug WITH user prefix>(Flow B) — NOTpaidapi. Theproject_slugis what
links the service to the project card (pricing badge, cross-navigation). Apaid_api
listing has no project association, so the project card will keep showing "Free".
Usepaid_apionly for truly external/standalone APIs with no Starchild project.
Forgot the link?updatethe service record withproject_slug— no need to re-list.
- Have an x402-enabled API — the endpoint must return
402when unpaid and200+ data
after a valid X-PAYMENT header. Use the x402 skill to implement this if needed.
#### Ensuring purchases are recorded by Starchild
For the Starchild marketplace to track purchases, earnings, and usage stats, choose one of the two approaches below based on your facilitator setup:
Option A — Use the Starchild facilitator (recommended)
Set your x402 middleware's facilitator URL to:
`` https://starchild-x402-facilitator.fly.dev ``
On successful settle, the Starchild facilitator automatically calls back community-gateway to record the purchase. No extra setup needed — proceed to step 2 with the default createpaidservice() call.
Option B — Use your own facilitator + proxy mode
If you use your own facilitator (or a third-party one), Starchild cannot receive settlement callbacks. Instead, pass source="manual" when creating the service record (step 2):
``python createpaidservice( ..., source="manual", # ← enables proxy mode ) ``
This tells the marketplace to generate a proxy URL for your API:
`` https://community.iamstarchild.com/proxy/{service_id}/... ``
Users access your API through this proxy URL. The proxy transparently forwards requests to your real api_endpoint and, on successful payment (HTTP 200 with a payment-signature header), automatically records the purchase in Starchild's database. You do NOT need to change your facilitator URL or set up any callbacks.
402 response requirements (checked during review):
- The 402 response body must include a pricingModel field (platform format). - payTo must be your actual receiving EVM wallet address (used on every enabled chain). - The accepts array contains one entry per enabled chain (multi-accepts); buyers pick one chain per payment. Each entry has the same amount (USDC, 6 decimals) — the platform does not support per-chain pricing in this release. - The response must be a valid x402 challenge that clients can parse.
- Create the service record (
servicetype = "paidapi"):
create_paid_service(
name="On-chain Whale Tracker API",
description="REST API returning real-time whale wallet movements across 12 chains.",
service_type="paid_api",
tags=["onchain-data", "wallet-portfolio", "trading"],
api_endpoint="https://api.example.com/v1/whales",
provider_wallet="0xAbC...yourEvmWallet", # EVM address for Base/Monad/Robinhood/X Layer; Solana address auto-fetched from Privy wallet
pricing_model="pay_per_use",
price=0.01,
free_trial_count=3,
api_documentation="# Whale Tracker API\n\n## GET /v1/whales\n\nReturns recent whale transactions.\n\n### Parameters\n| name | type | required | description |\n|---|---|---|---|\n| chain | string | no | Filter by chain id (default: all) |\n| limit | int | no | Max results (default: 50, max: 200) |\n\n### Response\n```json\n[{\"hash\":\"0x...\",\"from\":\"0x...\",\"to\":\"0x...\",\"value\":\"1000000\",\"token\":\"USDC\",\"chain\":\"base\",\"ts\":1700000000}]\n```",
example_request="curl https://api.example.com/v1/whales?chain=base&limit=10",
example_response='[{"hash":"0xabc...","from":"0x111...","to":"0x222...","value":"5000000","token":"USDC","chain":"base","ts":1700000000}]',
)
Required paid-API fields: name, description, servicetype, apiendpoint, providerwallet, pricingmodel, price, apidocumentation. Recommended (optional): examplerequest, exampleresponse (improves buyer experience). Optional: freetrialcount (only for payperuse), source ("manual" for proxy mode — see step 1 Option B above; omit for default Starchild facilitator mode), coverurl (custom cover image URL — must be on storage.googleapis.com or other allowed domains; if not provided, the agent should auto-generate a suitable cover image based on the service name and description, upload it via the image upload service, and pass the resulting URL).
#### Cover image for paid services
Paid services do NOT auto-generate a cover image (unlike free projects which get auto-captured screenshots). Pass coverurl in createpaid_service() — must be on storage.googleapis.com (or image.thum.io / api.microlink.io).
⚠️ MANDATORY: When the user provides an image or you need to set a cover, call uploadcoverimage(slug, file_path). This function handles the full flow: presign URL → compress → upload to GCS → return storage.googleapis.com public URL. Do NOT use imgur, data URIs, or any other hosting — the gateway validates the domain.
If the user does not provide an image, generate one (e.g. using an image generation skill), save it locally, then call uploadcoverimage().
You can also use updateservice(coverurl=...) later to change the cover.
See [Cover Image Upload](#cover-image-upload-flow) for the complete reference.
- Run review → same as Flow B step 4 (required before publishing).
- Publish → same as Flow B step 5 (requires approved status).
Flow D — Free Webpage + Paid API (hybrid)
Your project has a free landing page (published via publish_preview()) AND a paid API endpoint. Users can browse the project page for free, but API calls cost money. The marketplace shows a single merged card with both "Visit Project" and "Call API" buttons.
- Publish the project via
publish_preview()— this creates the free landing page. - Configure x402 charging on the API endpoint (e.g.
/api/randomreturns 402). - Create the service record with
servicetype="paidproject"+project_slug:
create_paid_service(
name="Random9 API",
description="Random 9-digit number API. Free docs page + paid API calls.",
service_type="paid_project",
tags=["developer-tools"],
project_slug="33-random9-api", # FULL slug WITH user prefix — links to the free project page
api_endpoint="https://community.iamstarchild.com/33-random9-api/api/random",
provider_wallet="0xAbC...yourEvmWallet", # EVM address for Base/Monad/Robinhood/X Layer; Solana address auto-fetched from Privy wallet
pricing_model="pay_per_use",
price=0.01,
service_description="Paid access to the Random9 API endpoint; the docs page stays free.", # required for paid_project
api_documentation="# Random9 API\n## GET /api/random\nReturns a random 9-digit number.",
example_request="curl https://community.iamstarchild.com/33-random9-api/api/random",
example_response='{"random":"482917365","digits":9}',
)
The project_slug merges this service into the project card. The project page (/) stays free; only the API endpoint (/api/random) requires payment.
> Note: if servicetype="paidapi" is passed together with projectslug, > createpaidservice() auto-upgrades it to paidproject and returns a > projectslugwarning — the stored listing is always paidproject. Passing > paidproject directly (as above) is the canonical form.
- Publish + optional self-check — same as Flow B steps 4–5.
Flow E — Multi-Endpoint API
Your service has multiple API endpoints at different prices (e.g. basic $0.01, premium $0.10). Each endpoint is listed separately in the marketplace detail view.
⚠️ When the user asks for multiple APIs, create ONE service with
api_endpoints— NOT multiple separate services.
For example, if the user says "develop three paid APIs and list them", do NOT callcreatepaidservice()three times. Instead, create a single service with anapi_endpointsarray containing all three endpoints. This gives users a unified
marketplace card where they can see and purchase individual endpoints.
Only create multiple services if the APIs are truly unrelated (different domains,
different audiences, different pricing models).
- Configure x402 charging with per-route pricing:
``bash # Default networksmode is "all" (Base + Monad). Omit --networks to follow # the platform mainnet set; pass --networks eip155:8453 only if the user # explicitly wants to restrict to a single chain. python3 skills/x402/scripts/monetize.py --name my-api --upstream-port 5173 \ --mode payper_use --price 0.01 \ --route "GET /api/basic=$0.01" --route "GET /api/premium=$0.10" \ --route "POST /api/batch=$0.50" \ --facilitator $FAC ``
- Create the service record with
api_endpoints:
create_paid_service(
name="Data API Service",
description="Multiple API endpoints at different prices.",
service_type="paid_api",
tags=["data-service"],
api_endpoint="https://example.com/api/basic", # primary endpoint for review
api_endpoints=[
{"path": "GET /api/basic", "price": 0.01, "label": "Basic Query"},
{"path": "GET /api/premium", "price": 0.10, "label": "Premium Query"},
{"path": "POST /api/batch", "price": 0.50, "label": "Batch Process"},
],
provider_wallet="0xAbC...yourEvmWallet", # EVM address for Base/Monad/Robinhood/X Layer; Solana address auto-fetched from Privy wallet
pricing_model="pay_per_use",
price=0.01, # price of the primary/default endpoint
api_documentation="# Data API\n## GET /api/basic\nBasic data.\n## GET /api/premium\nPremium analytics.",
example_request="curl https://example.com/api/basic",
example_response='{"data":"basic market info"}',
)
You can combine Flow D + Flow E: use servicetype="paidproject" + projectslug together with apiendpoints to link a free project page with multi-endpoint pricing. The marketplace shows a merged project card with an endpoint list in the detail view.
- Publish + optional self-check — same as Flow B steps 4–5.
Review checks (6 automated checks — required for publishing)
submitforreview() runs these checks against the api_endpoint; the service must pass all checks to be approved for publishing. A check run against an already-listed service never delists it:
| # | Check | What it verifies |
|---|---|---|
| 1 | api_reachable |
The endpoint returns 402 Payment Required when no X-PAYMENT header is sent |
| 2 | pricing_consistency |
The amount in the 402 response's accepts matches the price you declared (in USDC base units). With multi-chain accepts (one entry per enabled chain), each entry must carry the same amount — the platform does not support per-chain pricing in this release. |
| 3 | x402_payment |
After a valid x402 payment, the endpoint returns 200 + data |
| 4 | response_match |
The actual response's key fields match your example_response |
| 5 | doc_completeness |
api_documentation includes parameter descriptions, response format, and at least one example |
Check #5 is keyword-matched: the doc must contain a "Response" (or "响应格式") section with actual body text under the heading — an empty section fails review. servicedescription (paidproject) and apidocumentation / examplerequest / exampleresponse (paidapi) are enforced at call time by createpaidservice(), which errors before creating an unreviewable record.
Common rejection causes:
- 402 response
amountdoesn't match declaredprice(off by decimals / wrong unit). - Endpoint doesn't return 402 at all (x402 not wired up, or returns 200 to unauthenticated requests).
example_responsedoesn't match what the API actually returns after payment.- Documentation missing parameter table or response schema.
Pricing models
All paid services use the x402 exact payment scheme (on-chain USDC/USDG settlement on the platform's enabled networks — by default Base + Monad + Robinhood + X Layer + Solana, following networks_mode="all"). Gas for settlement is paid by the Starchild facilitator, not the provider.
pricing_model |
Meaning | x402 behavior | Typical use |
|---|---|---|---|
payperuse |
Per-call charge | Every request with valid X-PAYMENT → settle (charge) |
API calls |
lifetime |
One-time buyout | First payment settles; subsequent requests verify past settlement, no re-charge | One-time purchases |
monthly |
Monthly subscription | Settles once per billing month; re-charge after expiry | Web subscriptions, API monthly plans |
weekly |
Weekly subscription | Settles once per 7 days; re-charge after expiry | Short-term subscriptions |
quarterly |
Quarterly subscription | Settles once per 90 days; re-charge after expiry | Quarterly plans |
yearly |
Yearly subscription | Settles once per 365 days; re-charge after expiry | Annual plans (often discounted) |
prepaid |
Prepaid balance | User deposits via deposit-settle (one on-chain tx), then each call debits balance off-chain (zero gas) |
High-frequency micro-payments |
freetrialcountis only valid forpayperuse— allows N free calls before charging.
It is not a calendar free promo. Time-window free (freepromo*) → x402 skill
(selling.md→ Limited-time free promotion).
Multi-plan (multiple pricing options)
A service can offer multiple pricing plans simultaneously (e.g. weekly + monthly + yearly). Pass pricing_options array when creating the service:
create_paid_service(
...,
pricing_options=[
{"pricing_model": "weekly", "price": 3, "is_default": True, "label": "Weekly"},
{"pricing_model": "monthly", "price": 10, "label": "Monthly"},
{"pricing_model": "yearly", "price": 90, "label": "Yearly (Save 42%)"},
],
)
Rules:
payperusecannot be combined with other pricing models.- Subscription models (weekly/monthly/quarterly/yearly) can be freely combined.
lifetimeandprepaidcan be combined with subscription models.- One option must be marked
is_default: True(or the first is auto-marked). - The service's
pricing_modelandpricefields are auto-synced to the default option.
Multi-plan 402 requirement: The service's x402 middleware must support the X-Pricing-Model header — when a client sends X-Pricing-Model: yearly, the 402 response must return the yearly plan's price. Review verifies each plan's 402 amount individually.
Reference: See x402-facilitator/docs/pricing-models.md for the full specification.
Restricting payment to specific chains (custom networks)
The default networks_mode="all" follows the platform mainnet set (Base + Monad + Robinhood + X Layer + Solana). Only restrict to a subset when the user explicitly asks for it ("only accept Base", "don't take Monad payments", etc.):
# Create a service that ONLY accepts Base USDC (not Monad)
create_paid_service(
...,
networks_mode="custom",
supported_networks=["eip155:8453"], # CAIP-2 chain id; non-empty required
)
# Switch an existing service from all → custom (only Monad)
update_service(service_id, networks_mode="custom", supported_networks=["eip155:143"])
# Switch back to all (follow platform mainnets; clears the custom list)
update_service(service_id, networks_mode="all")
Do NOT default to custom + ['eip155:8453']. That re-introduces the old Base-only behavior. The default is all; only use custom when the user explicitly restricts.
Service Examples (API call examples) — recommended but optional
API call examples are optional but strongly recommended. They show buyers what the API returns — appearing as collapsible request/response pairs on the service detail page. Services without examples will still pass review, but the review report will note that examples are missing.
Recommended listing order:
1. create_paid_service(...) → creates the service
2. set_service_examples(service_id, examples) → recommended (improves buyer experience)
3. submit_for_review(service_id) → required before publishing
4. publish_service(service_id) → go live (requires approved)
Adding examples:
set_service_examples("service-uuid", [
{
"title": "Query BTC Price",
"description": "Get current Bitcoin price in USD",
"request": 'curl -X GET "https://api.example.com/v1/price?symbol=BTC"',
"response": '{"symbol": "BTC", "price": 67234.56, "currency": "USD"}'
},
{
"title": "Query ETH Price",
"request": 'curl -X GET "https://api.example.com/v1/price?symbol=ETH"',
"response": '{"symbol": "ETH", "price": 3456.78, "currency": "USD"}'
}
])
Clearing examples (rarely needed — setserviceexamples replaces all):
clear_service_examples("service-uuid")
Best practices:
- Add 2-5 examples covering the most common use cases
- Use descriptive titles that explain the scenario
- Include realistic request parameters and response data
- Show both simple and complex usage patterns
setserviceexamples()replaces ALL examples — pass the complete list every time
This supersedes the legacy single examplerequest / exampleresponse fields passed in createpaidservice(). Services with legacy fields still pass the review (backward compatible), but new services should use setserviceexamples() for richer multi-scenario demonstrations.
Paid service management functions
| Function | Purpose |
|---|---|
createpaidservice(...) |
Create a service record (published state) |
setserviceexamples(service_id, examples) |
Set API call examples (optional, recommended) — replaces all examples |
clearserviceexamples(service_id) |
Remove all API call examples |
submitforreview(service_id) |
Run the 6-check automated review (required before publishing) |
getreviewstatus(service_id) |
Poll review progress + per-check details |
publishservice(serviceid) |
Go live (requires approved or unlisted state) |
unpublishservice(serviceid) |
Take down (listed → unlisted) |
listmyservices(cursor, limit) |
List your services (paginated) |
getservice(serviceid) |
Fetch one service by ID |
updateservice(serviceid, **fields) |
Update service fields (e.g. fix after rejection) |
deleteservice(serviceid) |
Permanently delete a service |
restoreservice(serviceid) |
Restore an unavailable service back to listed |
Marketplace browse & consumer functions
These functions let the agent browse the Service Marketplace, read reviews, write reviews, manage favorites, and check earnings — same as the web frontend.
| Function | Purpose |
|---|---|
exploremarketplace(search, paidonly, ...) |
⭐ UNIFIED browse — use this FIRST to find paid services/APIs. Project cards + standalone services in one feed (same as web All/Paid tabs); the only search path that surfaces services merged into public project cards. Items have type: service (use id) or project (paid cards carry serviceid) — feed into getservice_detail() |
explore_services(search, sort, tags, ...) |
Browse STANDALONE service items only (services API). ⚠️ Services merged into a public project card do NOT appear here — use explore_marketplace() for full coverage |
getservicedetail(service_id) |
Public detail for a published service (includes docs, increments views) |
getservicepricing(service_id) |
Verified pricing with real-time x402 check |
getservicereviews(service_id, sort) |
List reviews for a service (public) |
writeservicereview(service_id, rating, comment) |
Submit/update a review (must have purchased or used first) |
getuserservices(user_id) |
Get a user's published paid services (public, for profile display) |
favoriteservice(serviceid) |
Add a service to favorites |
unfavoriteservice(serviceid) |
Remove a service from favorites |
getfavoriteservices(cursor, limit) |
List the current user's favorite services |
getservicepurchasestatus(serviceid) |
Check if the current user has purchased/used a service |
getserviceearnings(service_id) |
Earnings stats for a single service (owner only) |
getearningssummary() |
Earnings summary across all services (owner only) |
getserviceonchainrecords(serviceid) |
On-chain USDC settlement records (owner only) |
Usage from a bash block
python3 - <<'EOF'
import sys
# Prefer the registered skill tools (read this SKILL.md via read_file to
# load them) over hand-written imports of exports.py. If you DO need a
# direct import: the directory name has a HYPHEN, so dotted imports
# (`from skills.community_publish import ...`) raise ModuleNotFoundError.
# Use this sys.path pattern (or importlib.util.spec_from_file_location).
sys.path.insert(0, "/data/workspace/skills/community-publish")
from exports import (
# PUBLISH: public URL
publish_preview, unpublish_preview, list_published_previews,
# PUBLISH: open source code
open_source, remove_open_source, fork,
list_open_source, get_open_source, validate_open_source,
# LIST: free (project gallery)
list_in_dashboard, unlist_from_dashboard, get_listing_status,
# LIST: paid (service marketplace)
create_paid_service, submit_for_review, get_review_status,
publish_service, unpublish_service,
list_my_services, get_service, update_service, delete_service,
restore_service, set_service_examples, clear_service_examples,
# MARKETPLACE: browse + consumer actions
explore_marketplace, explore_services, get_service_detail,
get_service_pricing, get_service_reviews, write_service_review,
get_user_services, favorite_service, unfavorite_service,
get_favorite_services, get_service_purchase_status,
get_service_earnings, get_earnings_summary, get_service_onchain_records,
# Manual repair (rare)
link_to_listing,
)
# Step 1: Publish the URL
print(publish_preview(preview_id="my-app-a3f1", slug="my-app"))
# Step 2a: Free listing — show on gallery
print(list_in_dashboard(slug="33-my-app", name="My App", description="A cool app"))
# OR Step 2b: Paid listing — create service + review + publish
res = create_paid_service(
name="My Paid App",
description="Premium features",
service_type="paid_project",
tags=["developer-tools"],
project_slug="33-my-app", # full published slug WITH user prefix
api_endpoint="https://community.iamstarchild.com/33-my-app",
provider_wallet="0xAbC...",
pricing_model="monthly",
price=5,
service_description="Subscribers get premium features.",
)
print(res)
# Then: publish_service(res["service_id"]) — optionally submit_for_review() first for a self-check report
EOF
Behavioral rules
- Show the diff before
opensource(). Aftervalidateopen_source, summarize what's about to be pushed and ask for confirmation. Exception: explicit "publish without confirmation" or re-publish of a known good project. - Never auto-run setup.sh on fork. Show the command, let the user confirm.
- Always collect env in one batch on fork. Read project's
envrequired, diff againstworkspace/.env, callrequestenv_inputONCE with the missing keys. - Review is required for publishing.
publishservice()requiresapprovedstatus — always runsubmitforreview()first. Show the report to the user; if rejected, fix withupdateservice()and re-runsubmitforreview(). A check run against an already-listed service never delists it. api_endpointmust be the x402 charge endpoint. For paid projects this is the project's public URL. For paid APIs it's the external API URL. The reviewer hits this URL expecting a402.- Price unit is USDC. The 402 response's
accepts.amountis in base units (6 decimals for USDC). A$0.01price →amount: "10000". Mismatch here is the #1 review failure. - Don't fabricate review results. Always call
getreviewstatus()to check — never assume the review passed because you submitted it. - Don't conflate publish and list.
publishpreview()allocates a URL.listindashboard()/createpaid_service()makes it discoverable. These are separate, deliberate steps. - Slug rules: lowercase alphanumeric + hyphens, 3-50 chars, no leading/trailing hyphen.
- Version rules (
open_source): strict semver. Re-publishing same version is rejected. - URL ≠ code ≠ listing: a public URL going down does NOT remove the open-source code or the marketplace listing, and vice versa. They're independent.
- Do NOT pass
projectslugfor standalonepaidapiservices.projectslugbelongs topaidprojectonly — including the "free webpage + paid API" pattern (Flow D, which usespaidproject; passingpaidapi+projectsluggets auto-upgraded topaidprojectwith aprojectslugwarning). Passing a preview slug or non-existent slug for a standalone API creates a phantom association. The backend silently clears non-existent slugs, but you should not passproject_slugunless the user explicitly wants to link a free project page with the paid API. - When the user asks for multiple APIs, create ONE service with
apiendpoints. Do NOT callcreatepaidservice()multiple times for related APIs. Use theapiendpointsarray to list all endpoints in a single service (Flow E). Only create multiple services if the APIs are truly unrelated (different domains, different audiences, different pricing models). - Default payment networks to
all. Never hard-code a single chain (e.g.['eip155:8453']) as the default — that re-introduces the old Base-only behavior. Omitnetworksmode/supportednetworks(or passnetworksmode="all") so the service follows the platform mainnet set (Base + Monad + Robinhood + X Layer + Solana; new chains picked up automatically). Only usenetworksmode="custom"+ a non-emptysupported_networkswhen the user explicitly asks to restrict to a subset ("only Base", "only Monad", etc.). providerwalletis an EVM address used on every enabled EVM chain. The Starchild facilitator settles to the same address on each EVM chain; it is NOT Base-only. Do not describe it as a "Base wallet" to the user. For Solana payments, the platform automatically uses the user's Privy Solana wallet address (providersolwallet). If the user has not explicitly provided a Solana address,createpaid_service()auto-fetches it from the Privy wallet. Services without a Solana address will not accept Solana payments (Solana is excluded from the 402 accepts list).- Gas for settlement is paid by the platform, not the provider. Do not tell the provider they need to fund ETH/MON for settler gas.
Common gotchas
| Symptom | Cause | Fix |
|---|---|---|
publish_preview: Preview not found |
Wrong preview_id, or service was stopped | Check /data/previews.json, restart with preview(action='serve') |
publish_preview: 429 Too many published previews |
Hit 20-per-user gateway cap | unpublish_preview() something old first |
publishpreview: FLYMACHINE_ID not set |
Running locally, not in Starchild container | URL publish only works in the production container |
listindashboard: 404 No preview found |
publish_preview() hasn't run for this slug yet |
Call publish_preview() first |
open_source: 400 Validation failed: env names not in .env.example |
Listed MYKEY in envrequired but forgot .env.example |
Add the missing key to .env.example |
open_source: 400 Possible secret detected |
Secret scanner found a real-looking API key | Move to env var; .env.example value should be your-key-here |
| Marketplace shows service as free / missing after publish | Only publish_preview() was run — URL publish ≠ paid listing |
Complete the chain: createpaidservice → publish_service |
createpaidservice: 400 Free services should be published through the Project publish flow |
Tried servicetype: "freeproject" |
Use listindashboard() for free projects, not createpaidservice() |
publish_service: 400 not in a publishable state |
Service is already listed, unavailable, or deleted | Check getservice() state; unavailable → restoreservice() |
submitforreview: 400 Free services do not require review |
The service record was created as a FREE type — the paid payload was built by hand (missing servicetype/wallet/pricing) instead of via createpaid_service() |
Delete it and recreate with createpaidservice() (all paid fields are required positional args, so this cannot happen through the function) |
Review rejected: pricing_consistency failed |
402 response amount doesn't match declared price |
Ensure amount = price * 1000000 (USDC 6 decimals) |
Review rejected: api_reachable failed |
Endpoint doesn't return 402 | Wire up x402 charging on the endpoint first |
createpaidservice response has projectslugwarning |
Passed projectslug for a paidapi but the slug doesn't exist in project_listings |
Backend cleared it automatically. If this is a standalone API, don't pass projectslug. If you intended Flow D, publishpreview() + listindashboard() the project first, then update_service() with the correct slug. |
createpaidservice: 500 Failed to create service after delete→create cycles with the SAME name |
Deleted services keep their slug (soft delete), and slug generation only tries a limited number of suffixes — repeated delete/recreate with one name exhausts them | Do NOT wait and retry — the failure is permanent for that name. Use a different service name, or restoreservice(serviceid) + update_service() instead of delete+recreate |
| Created multiple services when user asked for "multiple APIs" | Called createpaidservice() once per API instead of using api_endpoints |
Use Flow E: one createpaidservice() call with api_endpoints=[...] array. Only split into multiple services if the APIs are truly unrelated. |
| Purchases not recorded for external API (own facilitator) | Service was created without source="manual", so no proxy URL is generated and Starchild has no way to observe payments |
Either switch to the Starchild facilitator (Option A) or recreate the service with source="manual" (Option B) |
Proxy URL returns 502 for source="manual" service |
The api_endpoint URL is unreachable from Starchild servers |
Verify the external API is publicly accessible and not behind a firewall |
Marketplace shows a chain the 402 accepts doesn't have (or vice versa) |
The service networks_mode and the x402 gateway's accepts got out of sync — e.g. listing is all (Base+Monad) but the x402 gateway was monetized with --networks eip155:8453 (custom, Base only) |
Re-run the x402 skill's monetize without --networks (to follow all), OR updateservice(serviceid, networksmode="custom", supportednetworks=[...]) to match the gateway. The two must agree. |
createpaidservice / updateservice rejects with "supportednetworks must be a non-empty list" |
networksmode="custom" was passed but supportednetworks was missing, empty, or None |
Pass a non-empty list of CAIP-2 ids (e.g. ["eip155:8453"]), or switch to networks_mode="all" (the default) to accept all platform mainnets. |
Buyer can't pay on Monad (402 has no Monad accepts) but listing shows Monad |
The x402 gateway was monetized before the multi-chain release, or with --networks eip155:8453 |
Re-run monetize without --networks so the 402 accepts array includes every platform mainnet. The listing all mode is correct; the gateway side is stale. |
| Settlement fails on one chain but works on another | The Starchild facilitator's settler is out of gas on that chain (ETH for Base, MON for Monad) | Platform-side issue (gas is subsidized). The other chain keeps working. Report to ops — do NOT ask the provider to fund gas. |
Cover Image Upload Flow
⚠️ MANDATORY — read this section whenever you need to set or change a cover image.
The gateway validates cover_url domains. Only storage.googleapis.com, image.thum.io, and api.microlink.io are accepted. Do NOT use imgur, data URIs, or any other hosting.
Quick path: uploadcoverimage()
from skills.community_publish.exports import upload_cover_image
result = upload_cover_image("my-slug", "/path/to/image.png")
# result = {"ok": True, "public_url": "https://storage.googleapis.com/..."}
# Then use the URL:
list_in_dashboard("my-slug", name="My Project", cover_url=result["public_url"])
# or:
create_paid_service(..., cover_url=result["public_url"])
# or:
update_service(service_id, cover_url=result["public_url"])
What uploadcoverimage() does internally
- Presign — calls
POST /api/projects/cover/presign(container JWT viaAuthorization: Bearer $CONTAINERJWT) withslug,contenttype,file_size - Upload — PUTs the raw image bytes to the GCS V4 signed URL
- Returns — the
public_urlonstorage.googleapis.com
Supported formats
image/png,image/jpeg,image/webp- Max 2MB — compress before uploading if needed
If the user provides a large image
# Compress first (PIL example)
from PIL import Image
import io
img = Image.open("/path/to/large.png")
img.thumbnail((1200, 630)) # reasonable cover dimensions
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=85)
buf.seek(0)
# Save compressed version
compressed_path = "/tmp/cover_compressed.jpg"
with open(compressed_path, "wb") as f:
f.write(buf.getvalue())
# Upload
result = upload_cover_image("my-slug", compressed_path)
Common mistakes
| Mistake | Why it fails | Fix |
|---|---|---|
Using imgur URL as cover_url |
Domain not in allowlist (project/internal routes reject with 400) | Use uploadcoverimage() → GCS URL |
Passing data URI as cover_url |
Gateway returns 500 (URL too long / not a valid URL) | Save to file, then uploadcoverimage() |
| Using thum.io to screenshot a preview page | Preview pages on internal ports are not publicly accessible | Use uploadcoverimage() with the actual image file |
| Not reading the knowledge doc | Missing context on GCS config, path conventions, domain rules | Always check starchild-knowledge/starchild-community-gateway/service-cover-upload.md |
Frontend presentation — Projects vs Services
The web frontend has two separate modals for community content:
| Modal | Content type | What it shows | Query functions |
|---|---|---|---|
| ProjectMarketplaceModal | Free projects | Card grid: cover image, name, description, tags, views, favorites | exploreprojects(), myprojects(), favoriteprojects(), gettabcounts(), getpopulartags(), getuserprojects(), favoriteproject(), unfavorite_project() |
| MarketplaceModal | Paid services (x402) | Service cards: pricing, ratings, purchase buttons | exploreservices(), listmyservices(), getservicedetail(), getservicetags(), getfeatured_services() |
Both also appear in AgentProfile (Projects tab / Services tab) and the landing page.
Each project has a direct URL (path-based /{slug} or subdomain {slug}.community.iamstarchild.com). The /projects and /services pages are frontend-rendered browse pages.
Projects query functions
| Function | Data scope | Purpose |
|---|---|---|
explore_projects(search, tag, sort, limit, cursor) |
Public | Browse all public projects. Sort: all (newest) or trending. |
my_projects(tag) |
Personal | List the current user's published projects with stats. |
favorite_projects(tag, limit, cursor) |
Personal | List the current user's favorited projects. |
gettabcounts() |
Personal | Get tab counts (explore, mine, favorites, purchased, servicesmine, servicesfavorites). |
getpopulartags() |
Public | Get popular project tags for filtering. |
getuserprojects(user_id, limit) |
Public | Get public projects by a specific user (for profile pages). |
favorite_project(slug) |
Personal | Add a project to the current user's favorites. |
unfavorite_project(slug) |
Personal | Remove a project from the current user's favorites. |
Services query functions
| Function | Data scope | Purpose |
|---|---|---|
explore_services(search, sort, tags, ...) |
Public | Browse standalone paid services only. |
listmyservices(cursor, limit) |
Personal | List the current user's paid services. |
getservicedetail(service_id) |
Public | Public detail for a published service. |
getservicepricing(service_id) |
Public | Verified pricing info (real-time x402 verification). |
getservicereviews(service_id, sort, cursor, limit) |
Public | List reviews for a service. |
writeservicereview(serviceid, rating, comment, isanonymous) |
Personal | Submit or update a review (upsert). |
getservicetags() |
Public | Get predefined service tags with i18n names. |
getfeaturedservices() |
Public | Get featured services for homepage display. |
getuserservices(user_id, limit) |
Public | Get published paid services by a specific user. |
favoriteservice(serviceid) |
Personal | Add a service to favorites. |
unfavoriteservice(serviceid) |
Personal | Remove a service from favorites. |
getfavoriteservices(cursor, limit) |
Personal | List the current user's favorite services. |
getservicepurchasestatus(serviceid) |
Personal | Check if user has purchased/used a service. |
getserviceearnings(service_id) |
Personal | Get earnings stats for a service (owner only). |
getearningssummary() |
Personal | Get earnings summary across all services. |
getserviceonchainrecords(serviceid, cursor, limit) |
Personal | Get on-chain transaction records (owner only). |
References
lib/manifest.py— project.yaml parser/writer + semver helperslib/validate.py— local pre-publish validation (mirrors gateway-side checks)lib/install.py— type-specific install handlers (task/service/script)lib/gateway.py— HTTP client for/api/register(URL),/api/code-projects/(code),/api/projects-query/(free listing + browse),/api/services/*(paid listing),/api/projects/cover/presign(cover upload)