Have Cursor or Claude Code turn a model repository into a live Ouro API, then register, test, and announce it without leaving the editor.
Updated · 8 min read
Putting a model on Ouro used to mean an afternoon of plumbing: read the repo, wrap inference in an API, deploy it, write the OpenAPI extensions, register the service, and test it by hand. A coding agent connected to Ouro can do the whole loop. It reads the model's code, writes the service, deploys it, registers it with the MCP server, runs it on a real file, and drafts the announcement.
What the agent doesn't know out of the box is how Ouro services are built: which headers Ouro sends, how to declare input and output assets, and when a route should be async. This guide shows how to give it that knowledge with a skill, then walks the loop from a model repository to a live, tested route.
A skill is a markdown file with instructions for one kind of task. The agent reads it when a request matches its description, so you don't repeat the conventions in every prompt.
The Ouro skills repository has one
for this job: matsci-modal-app builds Modal apps for open materials-science
models, with Ouro file inputs and outputs, webhook callbacks for long jobs, and
progress logging. It's how most of the models in
#materials-science got there. Install it for your
agent:
# Any agent, with the skills CLI
npx skills add ourofoundation/skills
# Cursor, for every project
git clone https://github.com/ourofoundation/skills.git ~/.cursor/skills/ouro
# Claude Code, for this project
git clone https://github.com/ourofoundation/skills.git .claude/skills/ouroIf your models aren't materials science, write your own skill. It doesn't need to be long. What matters is the handful of rules the agent would otherwise get wrong:
---
name: ouro-service
description: Build and deploy an HTTP API for a model and publish it as an Ouro
service. Use when wrapping a model or script as an Ouro route.
---
# Ouro services
- Build with FastAPI. Generate the spec with `get_custom_openapi` and declare
assets with `ouro_field` from `ouro.utils`.
- Declare file and dataset inputs with keyed `x-ouro-input-assets`, keyed by
the request body field that receives them, with exact `file_extensions`.
- Declare large outputs with `x-ouro-output-assets` and return them as base64
payloads with `org_id` and `team_id` from the `ouro-route-org-id` and
`ouro-route-team-id` headers.
- Keep the JSON response a small summary: scalars, counts, and verdicts.
- Anything that can take over five minutes returns 202 and reports back through
the `ouro-webhook-url` header.
- Log milestones with `action.log()`. Return 400s that name the bad field.
- Give every optional parameter a default, and describe when to use each route.Each rule links to a guide if you want the reasoning: route input and output assets, designing routes for agents, and long-running APIs.
Each step below is one prompt. You'll review code and results between steps, the way you would with a colleague.
Read the README and inference code in
github.com/example/crystal-model. What does it take as input, what does it return, what hardware does it need, and how long does one prediction take?
That last question decides the most important design choice. A prediction that finishes in seconds can return directly. One that takes twenty minutes has to be async, or the call times out.
Build a Modal app for it under
apps/crystal-model/following the Ouro service skill. One route that takes a CIF file and returns predicted properties, plus the relaxed structure as an output file.
Check the declarations before anything else. They're what let Ouro resolve a caller's file into your request body and save your outputs as assets:
@web_app.post(
"/predict",
summary="Predict formation energy and band gap",
description="Predicts properties of a crystal structure. Use to screen "
"candidates before running DFT.",
)
@ouro_field(
"x-ouro-input-assets",
{"structure": {"asset_type": "file", "file_extensions": ["cif"], "primary": True}},
)
@ouro_field(
"x-ouro-output-assets",
{"relaxed_structure": {"asset_type": "file", "file_extensions": ["cif"]}},
)
async def predict(request: PredictRequest, ...):
...Serve it with
modal serveand hit/openapi.json. Confirm the Ouro extensions are there. Then deploy it.
Running modal serve first gives you a temporary URL that reloads on every
save, so the agent can fix problems before anything is permanent.
modal deploy gives you the stable URL you'll register.
Create an Ouro service from the deployed spec in my team, with Ouro authentication. It wraps a third-party model; set the license and link the repo and paper.
The agent calls create_service with spec_url pointing at your
/openapi.json, and Ouro creates one route per endpoint. Attribution matters
here. The model isn't yours, so the service should say so: originality set to
third-party, the model's github_url and paper_url, and a license
compatible with the original. See
licensing and attribution
for the fields.
With Ouro authentication, Ouro sends a shared secret with every call it
forwards and your app rejects calls without it, so nobody can skip Ouro to use your deployment
directly. The service settings on Ouro hold the secret; store it in your
deployment as OURO_SERVICE_SECRET. The Modal
guide
has the validation code.
Run the predict route on a structure whose properties are published, like silicon. Dry run first. Compare the result to the literature value, and read the action logs if anything fails.
Test on something you already know the answer to before anything new. A route
that returns plausible numbers can still be wrong, and a known answer is the
only way to tell. Ouro records each run as an action: execute_route returns
its ID, get_action shows status and outputs, and get_action_logs shows what
your service logged along the way.
When something's wrong, the agent edits the code, redeploys, and re-syncs:
Fix the unit conversion, redeploy, and update the service from the new spec. Then rerun the silicon test.
update_service with the same spec_url re-reads the spec. Routes are matched
on method and path, so existing route IDs, history, and links survive the
update.
Draft a post announcing the service: what the model does, who made it, the silicon test run embedded with its action, and a link to the paper. Mention the authors if they're on Ouro.
A route embedded with a real run shows readers the service working on a real input, with the status, timing, and outputs of that run. It's the strongest announcement you can make. Review the draft and publish it. If you want to charge for the route, set pricing in the route's settings; see how to monetize APIs.
Every mistake the agent makes on one service, it will make on the next unless you write it down. When something goes wrong, add a line to the skill:
localhost file URLs to the production storage URL before downloading."@modal.enter(), not per request."A short skill full of hard-won rules beats a long one full of general advice.
Keep SKILL.md under 500 lines and move templates into a reference.md the
agent reads only when it needs them. If the skill would help others,
contribute it.
Before you announce a service, check that:
/openapi.json shows x-ouro-input-assets and x-ouro-output-assets on every route that uses assetsOn this page