Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/validate-frontmatter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ on:
paths:
- "**/*.mdx"
- "scripts/validate-frontmatter.mjs"
- "scripts/check-plan-limits.mjs"
- "package.json"
pull_request:
paths:
- "**/*.mdx"
- "scripts/validate-frontmatter.mjs"
- "scripts/check-plan-limits.mjs"
- "package.json"

jobs:
Expand All @@ -24,3 +26,5 @@ jobs:
node-version: "20"
- name: Run frontmatter validator
run: node scripts/validate-frontmatter.mjs
- name: Check plan-limit tables match the source of truth
run: node scripts/check-plan-limits.mjs
9 changes: 5 additions & 4 deletions concepts/credits.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ Per-job billing from a prepaid credit balance. New accounts get $5 free. Every t
| Signup credits | $5 (one-time) | - |
| Monthly credits included | - | $5 |
| Credit-purchase bonus | - | +20% |
| Concurrent jobs | 1 | 25 |
| API requests/min | 30 | 300 |
| Max input file | 100 MB | 2 GB |
| Concurrent jobs | 1 | 20 |
| Queued backlog | 50 | 1000 |
| API requests/min | 120 | 600 |
| Max input file | 500 MB | 10 GB |
| Job timeout | 5 min | 15 min |
| Output retention | 7 days | 30 days |
| Priority queue | No | Yes |
Expand All @@ -59,7 +60,7 @@ Every job is billed in nanodollars (one-billionth of a US dollar) drawn from you

## Credit lifecycle

1. **Submit gate.** Submit is atomic. If your balance is greater than zero and you are under your concurrent-job limit, the job runs. We do not estimate cost up front. Insufficient balance returns `402 INSUFFICIENT_CREDITS`.
1. **Submit gate.** Submit is atomic and does not reject on concurrency. If your balance is greater than zero the job is accepted. Jobs run at your plan's concurrency and any extra wait in a queue, starting in order as slots free. We do not estimate cost up front. A zero balance returns `402 INSUFFICIENT_CREDITS`, and a full queue returns `429 QUEUE_FULL`.
2. **Execution.** Job runs. Balance untouched.
3. **Debit on terminal status.** Every terminal job (`complete`, `failed`, or `cancelled`) is charged for the compute it consumed. The debit is atomic with the status update.
4. **No auto-refund.** Failed jobs are still charged for the compute they consumed. If you believe a job failed due to a platform bug, contact support. Refunds are issued at our discretion via the dispute process.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"type": "module",
"scripts": {
"validate:frontmatter": "node scripts/validate-frontmatter.mjs"
"validate:frontmatter": "node scripts/validate-frontmatter.mjs",
"validate:plan-limits": "node scripts/check-plan-limits.mjs"
}
}
91 changes: 91 additions & 0 deletions scripts/check-plan-limits.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env node
/**
* Plan-limit consistency guard for the docs.
*
* The single source of truth for plan tiers is the monorepo's
* packages/shared/src/constants/price-tiers.ts, published (generated) at
* https://rendobar.com/plan-limits.json. This script fetches that export and
* asserts the tier tables in concepts/credits.mdx and support/limits.mdx match
* it, so the docs can never silently drift from the real limits.
*
* If the published JSON is unreachable (e.g. it has not deployed yet), the
* check warns and exits 0 rather than blocking. Once live it enforces.
*
* Exit 0 = consistent (or source unreachable). Exit 1 = a table cell is stale.
*/

import { readFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..");
const SOURCE_URL = "https://rendobar.com/plan-limits.json";

const fmtInt = (n) => String(n);
const fmtBytes = (n) =>
n % 1024 ** 3 === 0 ? `${n / 1024 ** 3} GB` : `${n / 1024 ** 2} MB`;
const fmtMin = (sec) => `${sec / 60} min`;
const fmtDays = (n) => `${n} days`;

// label substring (lowercased) -> [limits key, formatter]
const ROWS = [
["concurrent jobs", "concurrentJobs", fmtInt],
["queued backlog", "maxQueuedJobs", fmtInt],
["api requests", "apiRequestsPerMinute", fmtInt],
["max input file", "maxInputFileSize", fmtBytes],
["max batch size", "maxBatchSize", fmtInt],
["job timeout", "maxJobTimeout", fmtMin],
["output retention", "outputRetentionDays", fmtDays],
["total r2 storage", "storageQuota", fmtBytes],
];

const FILES = ["concepts/credits.mdx", "support/limits.mdx"];

/** Find the `| label… | free | pro |` row and return the free/pro cells. */
function tierRow(md, labelSubstring) {
const line = md
.split("\n")
.find((l) => l.includes("|") && l.toLowerCase().includes(labelSubstring));
if (!line) return null;
const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
return { free: cells[1], pro: cells[2] };
}

async function main() {
let source;
try {
const res = await fetch(SOURCE_URL);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
source = await res.json();
} catch (err) {
console.warn(`⚠ plan-limits source unreachable (${SOURCE_URL}): ${err.message}. Skipping.`);
return; // exit 0 — let Node drain the socket pool cleanly
}

const errors = [];
for (const file of FILES) {
const md = readFileSync(join(ROOT, file), "utf8");
for (const [label, key, fmt] of ROWS) {
const row = tierRow(md, label);
if (!row) continue; // not every row is in every file
const wantFree = fmt(source.plans.free.limits[key]);
const wantPro = fmt(source.plans.pro.limits[key]);
if (row.free !== wantFree || row.pro !== wantPro) {
errors.push(
`${file}: "${label}" is | ${row.free} | ${row.pro} | but source says | ${wantFree} | ${wantPro} |`,
);
}
}
}

if (errors.length) {
console.error("✗ Plan-limit tables drifted from the source of truth:\n" + errors.map((e) => ` - ${e}`).join("\n"));
console.error(`\nUpdate the tables to match ${SOURCE_URL} (edit price-tiers.ts in the monorepo to change a value).`);
process.exitCode = 1;
return;
}
console.log("✓ Plan-limit tables match the source of truth.");
}

main();
4 changes: 2 additions & 2 deletions support/faq.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ canonical: "https://rendobar.com/docs/support/faq"
"name": "Is there a rate limit?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. 30 req/min (Free), 300 req/min (Pro). Applies to REST and MCP. Limit hit returns 429 RATE_LIMITED with a Retry-After header."
"text": "Yes. 120 req/min (Free), 600 req/min (Pro). Applies to REST and MCP. Limit hit returns 429 RATE_LIMITED with a Retry-After header."
}
},
{
Expand Down Expand Up @@ -116,7 +116,7 @@ canonical: "https://rendobar.com/docs/support/faq"
</Accordion>

<Accordion title="Is there a rate limit?">
Yes. 30 req/min (Free), 300 req/min (Pro). Applies to REST and MCP. Limit hit returns `429 RATE_LIMITED` with a `Retry-After` header.
Yes. 120 req/min (Free), 600 req/min (Pro). Applies to REST and MCP. Limit hit returns `429 RATE_LIMITED` with a `Retry-After` header.
</Accordion>

<Accordion title="What happens if a job fails?">
Expand Down
9 changes: 5 additions & 4 deletions support/limits.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ Every limit Rendobar enforces, by plan. Pulled from `PLAN_LIMITS` in the API. Fo
| Signup credits | $5 (one-time) | - |
| Monthly credits | - | $5/mo included |
| Credit-purchase bonus | - | +20% |
| Concurrent jobs | 1 | 25 |
| API requests / min | 30 | 300 |
| Max input file | 100 MB | 2 GB |
| Concurrent jobs | 1 | 20 |
| Queued backlog | 50 | 1000 |
| API requests / min | 120 | 600 |
| Max input file | 500 MB | 10 GB |
| Max batch size | 10 | 100 |
| Max job timeout | 5 min | 15 min |
| Output retention | 7 days | 30 days |
| Total R2 storage | 1 GB | 50 GB |
| Total R2 storage | 5 GB | 250 GB |
| Priority queue | No | Yes |

`ffmpeg`, MCP, and webhooks are on both plans.
Expand Down
Loading