media-proxy
Shared helper that routes ALL paid media generation (FAL image/video, ElevenLabs music) through the GooseWorks proxies so every call is charged to your GooseWorks credits — never a provider SDK's default host.
Install / Use
npx skills add gooseworks-ai/goose-skills --skill media-proxyInstalls into whichever agent you are using.
SKILL.md
Installable skill definition
Quality Score
Category
MarketingSupported Platforms
Our assessment of media-proxy
media-proxy scores 91/100 on our quality scale, 194th of 560 Marketing skills we index (top 35%).
Its SKILL.md is 15 KB long, well organised into 14 sections with 2 code examples: a thorough specification that gives an agent plenty to work with.
With 1,222 GitHub stars, it is one of the more widely adopted skills in the catalogue.
Maintenance, license and trust
- The repository was last updated 33 days ago, so media-proxy is actively maintained.
- Our last check on 2026-10-06 found the source still online.
- It is released under the MIT license, a permissive license that allows use, modification and commercial use with attribution.
- Its trust signals score 100/100, with no cautions. These come from repository metadata, not a code audit — read the skill file before letting an agent act on it.
Safety scan
No issues foundOur scan of the whole file found no instruction hijacking, hidden characters, credential access, data exfiltration or destructive commands.
Automated pattern scan on 2026-10-26. It catches known dangerous patterns, not every risk — read a skill before letting an agent act on it.
media-proxy compared with similar skills
All 4 of these similar skills score higher than media-proxy; compare them before choosing.
| Skill | Score | Stars | Updated | Format |
|---|---|---|---|---|
| media-proxy (this skill)by gooseworks-ai | 91 | 1.2k | 33d ago | SKILL.md |
| taste-skillby Leonxlnx | 100 | 102.4k | today | CLAUDE.md |
| LocalAIby mudler | 100 | 50.0k | today | MCP Server |
| algorithmic-artby anthropics | 100 | 177.9k | 33d ago | SKILL.md |
| pptxby anthropics | 100 | 177.9k | 33d ago | SKILL.md |
Frequently asked questions
- How do I install media-proxy?
- Run
npx skills add gooseworks-ai/goose-skills --skill media-proxy. The install tabs above show the steps for each supported agent. - Which AI agents does media-proxy work with?
- It is written for Universal, as a SKILL.md file. Other agents that read the same format can often use it too.
- Is media-proxy safe to use?
- Our scan of the whole file found no instruction hijacking, hidden characters, credential access, data exfiltration or destructive commands. It is MIT-licensed and scores 100/100 on trust signals. Skills are instructions an agent will follow, so read the file before installing it and do not approve commands you do not understand.
- Is media-proxy still maintained?
- The repository was last updated 33 days ago, so media-proxy is actively maintained.
Skill content
View source on GitHubname: media-proxy description: Shared helper that routes ALL paid media generation (FAL image/video, ElevenLabs music) through the GooseWorks proxies so every call is charged to your GooseWorks credits — never a provider SDK's default host. Host-swaps the FAL queue URLs, loads the agent token from the sandbox env (GW_MEDIA_PROXY_TOKEN) or ~/.gooseworks/credentials.json, and returns the result CDN URL. Every video-ad media capability imports this; templates never call a provider directly. status: active version: "2.0.6"
media-proxy
The foundation capability for paid media in the video-ad pipeline. It fixes the
auth-path conflict where engine scripts called FAL/ElevenLabs directly (billing
the wrong account): all paid calls now go through
<api_base>/api/internal/{fal-proxy,elevenlabs-proxy} with ?token=&agent_id=, which
is charged to your GooseWorks credits.
Crash-resume (never lose / double-bill a paid render)
A FAL submit BILLS immediately, but the local backend can blip during a multi-minute render. Two built-in protections (automatic for every capability that imports this):
-
Poll-through-outage —
_fal_run's poll loop re-attaches to the same status/result URL throughconnection refused/ timeout blips instead of crashing. -
Persist + resume — each submit's
request_id+ poll URLs are written to~/.gooseworks/pending-fal-jobs/(GW_FAL_PENDING_DIRoverrides, for tests). A caller that keeps its own manifest passeson_submit=tofal_generate*: it is called with therequest_idright after the submit is saved. If the poller still dies, re-attach instead of re-firing (re-firing double-bills):resume_fal(request_id)in Python, or the CLI:resume.py --list # resumable (submitted, unfinished) jobs resume.py --request-id <id> --out final.mp4 # poll to completion + downloadresume_falNEVER re-submits, so it can't double-charge. -
Poll timeout never resubmits — polling gives up after
default_poll_timeout(model): 1800s for video / lipsync / audio-driven models, 600s for images (GW_FAL_POLL_TIMEOUT_Soverrides;timeout_s=per call). A timeout raisesFalPollTimeoutcarrying.request_id+.model_path— the job is still running and already paid for. Re-attach withresume_fal(e.request_id); never callfal_generate*again for it (a veed/fabric lipsync once finished 26s after a 600s poller quit, and the retry paid for a second identical job). -
Proxy dedupe back-stop — the fal proxy returns the already-running job for an identical submit (same agent + model + body) within 30 min, so an accidental resubmit re-attaches instead of paying twice (response header
x-gw-deduped: 1). For a deliberate re-roll of the same input (want a new take), passnew_take=True(fal_generate(..., new_take=True)/fal_generate_video(...)), which sendsx-gw-no-dedupe: 1(raw HTTP callers: that header or?dedupe=0). -
Pass
input_digest=for any piece you save as an ingredient, so a resumed run never pays twice. The body-match dedupe above breaks after a sandbox restart: the resumed run re-uploads its inputs (voiceover, stills) and gets NEW urls, so the body differs.fal_generate*(..., input_digest=d)sendsx-gw-input-digest: d; the proxy then dedupes on (agent + model + digest) for 24 h and returns the job you already paid for. Use the same stable digest you save withmedia_upload(see below). If that job's result has since expired at fal, the poll fails: retry once withnew_take=True.
Policy rejections are final: surface, do not retry
When fal refuses a request on policy grounds, _fal_run (and so fal_generate*) raises
FalPolicyRejection, a RuntimeError subclass, so old except RuntimeError handlers
still work. Policy grounds means a likeness of a real person ("likenesses of real people",
"real person", "public figure"), content_policy_violation, partner_validation_failed, or
NSFW / safety checker.
-
Every body shape is read. fal's
detailas a list of{msg, type, loc, ctx}, a dict or a string; a top-leveltype/code/error; the GooseWorks MCP wrapper{"error": {"code": "provider_validation_failed", "status": 422, "detail": <fal body>}}; and a failedjob_getreply{"status": "failed", "error": "fal returned HTTP 422 for the result.", "result": {"detail": <fal body>}}(fal reports most failed generations as COMPLETED plus a 422 on the result). Before this, a body with bothmsgandtypekept the message and lost the type, so the rejection looked like a generic error. -
When it counts as policy. An explicit policy code (
content_policy_violation,partner_validation_failed,content_blocked) at any status, or a policy phrase (likeness, public figure, usage guidelines, risk control, moderation, blocked for safety, NSFW, flagged by the safety checker) on an explicit 4xx that is not 408 or 429. Input errors such as "Could not detect a real person's face" or "enable_safety_checker cannot be disabled" are not policy. A rejection on a 5xx, 408 or 429 is surfaced but never recorded. -
What it carries:
reason(the provider's words),kind(likeness|partner_validation|content_policy|nsfw),error_type,request_id,http_status,stage,chargedandcharge_note.chargedisFalsefor an explicit provider 4xx: the GooseWorks proxy debits only a successful response and releases the hold on a 4xx (read from the proxy code, not yet confirmed on a real rejection). It isNone(unknown) for a job that failed after it was accepted. -
The exact request is recorded in
~/.gooseworks/rejected-fal-requests/<key>.json, next topending-fal-jobs/(GW_FAL_REJECTIONS_DIRoverrides). The key is a digest of the model plus the canonical JSON payload (sorted keys), and also ofinput_digestwhen one is passed. -
An identical request is refused before any network call, with the same exception and
from_ledger=True: "surface, do not retry: this exact request was already rejected by the provider for ...; change the inputs (image, prompt, model) to try again".new_take=Truedoes not bypass it: a re-roll of a rejected payload is still the same payload. Any change to the prompt, an image, the seed or the model is a new request and is sent. -
Inputs re-uploaded each run get new URLs, so the payload digest never matches twice. Pass
input_digest=over the inputs' content, ascreate-creator-takes-h3/run_takes.pyandrender-street-interview/single_gen.pydo. The ledger then matches on that too.refuse_if_rejected(model, input_digest=d)checks before you upload anything. -
A caller's
input_digestis a permanent refusal key. It must cover EVERY input that is sent: the prompt, every setting (duration, resolution, aspect ratio, audio, seed) and the content of every input file. Simplest:input_digest(model, payload)over the real payload with each uploaded URL replaced by{"sha256": <file hash>}. Leave an input out and a run that changed only that input (a new, acceptable image) is refused. -
Exit code: scripts exit with
POLICY_EXIT(3). The MCP relay also exits 3; the relay prints[mcp-relay], a rejection prints "surface, do not retry". -
Non-policy errors (an unreadable image URL, a bad duration) stay a plain
RuntimeErrorwith the provider's message and type, and are never recorded. -
Every refusal names its record: the ledger file and whether it matched the exact payload or the caller's
input_digest. -
Clearing a record is a user-approved action. Only when the user explicitly approves, e.g. because the provider changed its policy:
python3 media_proxy.py rejections # list recorded rejections (key, model, reason, request id) python3 media_proxy.py forget <key> # clear one record (all its keys); USER-APPROVED ONLYforget_rejection(key)does the same in Python. It only ever deletes ledger records: it takes the 32-hex key (or that record's filename or path inside the ledger directory) and refuses anything else, such as another.jsonfile or a file without akeysfield. Agents never clear a record on their own to get a retry through.
Use it
from media_proxy import fal_generate, fal_generate_video, eleven_music, download
# image (nano-banana / gpt-image / etc.) — inputs must be PUBLIC urls
img = fal_generate("fal-ai/nano-banana/edit",
{"prompt": p, "image_urls": [product_url], "aspect_ratio": "9:16"})
# video i2v (kling / seedance / veo)
vid = fal_generate_video("fal-ai/kling-video/v3/pro/image-to-video",
{"prompt": p, "image_url": keyframe_url, "duration": "10"})
# music bed
eleven_music(prompt, 10500, "music.mp3", force_instrumental=True)
Save-as-you-go digest
input_digest(model, args) = sha256 of the canonical JSON {"model", "args"} (sorted
keys, no whitespace), first 32 hex chars. Pass it with the MCP media_upload of the
result (plus an ingredient_key such as vo/scene-03); on a resume, media_list { ingredient_key } returns the saved file and its digest, and the file is reused only
when the digest of the args you would send now is the same.
from media_proxy import input_digest, eleven_tts, fal_generate_video
args = {"text": line, "voice_id": vid, "model_id": "eleven_v3"}
digest = input_digest("elevenlabs/tts", args)
# FAL job: hash the STABLE identities of its inputs, not their urls, and send the
# same digest with the submit so a resumed run re-attaches instead of re-paying.
lip_digest = input_digest("veed/fabric-1.0", {"image": "still/scene-03:" + still_digest,
"audio": "vo/scene-03:" + digest})
clip = fal_generate_video("veed/fabric-1.0", {"image_url": still_url, "audio_url": vo_url},
input_digest=lip_digest)
Hash only what determines the output. Swap any expiring input URL (presigned / proxy) for that input's own ingredient_key + digest first, or the digest never matches.
Contracts (load-bearing)
- Charged to your GooseWorks credits —
?token=&agent_id=from~/.gooseworks/credentials.json(written by the GooseWorks CLI). In a GooseWorks cloud sandbox (coworker chat) the env wins instead:GW_MEDIA_PROXY_TOKEN(a per-session token that already binds agent/org/user) +GW_API_BASE;GW_PROJECT_IDattributes spend. - Host-swap the FAL queue URLs — submit returns
status_url/response_urlonqueue.fal.run; the helper rewrites them to the proxy base (keeps the path). Never pollqueue.fal.rundirectly (401 + burns credits). - FAL inputs that are local files must be PUBLIC urls. The orchestrator hosts a
local image/audio with the MCP
media_upload(its returned url) and passes THAT url in, or use this module'sfal_upload(path)function, which puts the file on the fal CDN through the fal-storage-proxy (free, verified 2026-09-28) and returns its public url. - Only the final
*.fal.mediaurl is a real public URL — everything else is behind the proxy.
No credentials at all? The MCP relay
A session that only has the GooseWorks MCP connector (a chat app, or a terminal where
the GooseWorks CLI is not signed in) has neither GW_MEDIA_PROXY_TOKEN (the cloud sandbox's)
nor ~/.gooseworks/credentials.json, so scripts cannot reach the proxies over HTTP. A
connected terminal session can also hold CLI credentials for a different environment; set
GW_MEDIA_VIA=mcp before running its media helpers so the selected connector stays
authoritative. Either way every paid call is relayed through the agent:
- The script writes the exact MCP tool call to
working/mcp-requests/<kind>-<hash>.jsonand exits with code 3, printing what to do. - The agent makes that
Truncated for display — read the full file on GitHub.
Related Skills
taste-skill
102.4kAgent skills that give coding agents taste: art direction, reference images and a browser check instead of generic AI pages
LocalAI
50.0kLocalAI is the open-source AI engine. Run any model - LLMs, vision, voice, image, video - on any hardware. No GPU required.
algorithmic-art
177.9kCreating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems.
pptx
177.9kUse this skill any time a .pptx or .potx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx or .potx file (even if the extracted content will be used elsewhere, like in an em…
Languages
Trust signals
From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.
