KestrelDocs

Getting started

Kestrel bills for runs, not for seats nobody uses. Usage is metered in sandbox minutes and model tokens, and settled once a month.

Fig. 1Usage billing
Usage billing Usage drops fall into a graduated basin and raise its level a step at a time as the month's days pass; at the month's end the basin drains through a narrow outlet into a meter that prints the invoice slip line by line, and the total shows once it is out. float xl(float y) { return 90.0 + 45.0 * (y - 80.0) / 215.0; } float xr(float y) { return 310.0 - 45.0 * (y - 80.0) / 215.0; } // the slip's print at its own row r (0 at the top) and column j (0 to 17) float slip(float r, float j) { // pale paper, so the print reads: a header, four line items a row apart, a rule, the total, a torn foot float P = 0.12; float v = P; if (j < 0.5 || j > 16.5 || r < 0.5) v = 0.6; // its edges else if (r < 1.5) v = j > 1.5 && j < 9.5 ? 1.15 : (j > 11.5 && j < 15.5 ? 0.6 : P); // header, date else if (abs(r - 4.0) < 0.5 || abs(r - 16.0) < 0.5) v = mod(j, 2.0) < 0.5 ? 0.6 : P; // rules else if (r > 5.5 && r < 14.5 && mod(r - 6.0, 3.0) < 0.5) { // four line items: what was used, and its amount float len = floor(hash(vec2(r, 3.0)) * 4.0); v = j > 1.5 && j < 5.5 + len ? 0.95 : (j > 11.5 && j < 15.5 ? 1.05 : P); } else if (abs(r - 19.0) < 0.5) v = j > 1.5 && j < 6.5 ? 1.15 : (j > 9.5 && j < 15.5 ? 1.2 : P); // the total else if (abs(r - 23.0) < 0.5) v = mod(j, 2.0) < 0.5 ? 0.6 : 0.0; // torn foot return v; } float field(vec2 p, float t) { if (p.x < 18.0 || p.x > 702.0 || p.y < 18.0 || p.y > 442.0) return 0.0; float ph = TAU * t / T; float k = floor(p.y / 9.4); float c = floor((p.x - 4.0) / 7.0 + 0.5); float v = 0.0; // the level, in graduations: up one per landed drop, down as it drains float lvl = ease((t - 0.47) / 0.15) + ease((t - 0.77) / 0.15) + ease((t - 1.07) / 0.15) + ease((t - 1.37) / 0.15) + ease((t - 1.67) / 0.15) + ease((t - 1.97) / 0.15); float dr = ease((t - 2.3) / 1.1); lvl -= 6.0 * dr; float surf = 31.0 - 3.0 * lvl; // the surface row float draining = step(2.3, t) * step(t, 3.4); float open = sm(2.25, 2.32, t) * (1.0 - sm(3.5, 3.6, t)); // the feed rail: a churning manifold over a dense bar, six nozzles under it if (p.x > 126.0 && p.x < 262.0) { if (k == 3.0) v += 0.3 + 0.3 * noise(vec2(p.x * 0.09 + 2.0 * cos(ph), 2.0 * sin(ph))); if (k == 4.0) v += 1.05; if (k == 5.0 && mod(c - 19.0, 3.0) < 0.5) v += 0.6; } // the basin: slanted walls with a lip, a floor with the outlet in it float L = xl(p.y), R = xr(p.y); if (k >= 8.0 && k <= 30.0) { if ((p.x >= L - 8.0 && p.x < L) || (p.x > R && p.x <= R + 8.0)) v += 1.05; else if ((p.x >= L - 16.0 && p.x < L) || (p.x > R && p.x <= R + 16.0)) v += 0.7; if (k == 8.0 && ((p.x >= L - 30.0 && p.x < L - 16.0) || (p.x > R + 16.0 && p.x <= R + 30.0))) v += 1.0; } if ((k == 31.0 || k == 32.0) && p.x >= L - 16.0 && p.x <= R + 16.0) { if (c == 28.0) v += 1.05 * (1.0 - open) + 0.95 * open; else v += k == 31.0 ? 1.0 : 0.7; } // graduations: ticks out from the left wall, long every other one, and a dotted level line inside at each for (int i = 0; i <= 6; i++) { float gk = 31.0 - 3.0 * float(i); if (k == gk && i > 0) { bool major = mod(float(i), 2.0) < 0.5; if (p.x < L - 16.0 && p.x >= L - 16.0 - (major ? 21.0 : 10.0)) v += major ? 0.7 : 0.6; if (p.x > L + 3.0 && p.x < R - 3.0 && mod(c, 2.0) < 0.5) v += 0.18; } if (k == gk && i == 0 && p.x < L - 16.0 && p.x >= L - 37.0) v += 0.7; } // the contents: a dense surface, denser with depth, streaming toward the outlet while it drains if (k < 31.0 && p.x >= L && p.x <= R && k >= surf - 0.5) { float depth = clamp((k - surf) / 18.0, 0.0, 1.0); v += k < surf + 0.5 ? 1.05 : 0.42 + 0.3 * depth; float d = length((p - vec2(200.0, 294.9)) / vec2(1.0, 1.6)); v += draining * 0.35 * step(0.62, fract(d / 26.0 + 5.0 * t)) * sm(2.3, 2.4, t); } { // drop 1: forms at its nozzle, falls gathering speed, lands on the surface, ripples out float u = (t - 0.15) / 0.32; float g = (t - 0.03) / 0.12; if (g > 0.0 && u < 0.0) v += blob(p, vec2(179.0, 58.0), 8.0, 1.1 * g); if (u >= 0.0 && u < 1.0) v += blob(p, vec2(179.0, mix(58.0, 285.4, u * u)), 9.0, 1.15 * (1.0 - sm(0.9, 1.0, u))); float w = (t - 0.47) / 0.4; if (w > 0.0 && w < 1.0) { float sk = 27.0; if (k == sk && p.x > L && p.x < R) v += 0.7 * (1.0 - w) * exp(-pow(abs(p.x - 179.0) - 120.0 * w, 2.0) / 120.0); if (k == sk - 1.0 && w < 0.4 && abs(abs(p.x - 179.0) - 9.0) < 4.0) v += 0.7; } } { // drop 2: forms at its nozzle, falls gathering speed, lands on the surface, ripples out float u = (t - 0.45) / 0.32; float g = (t - 0.33) / 0.12; if (g > 0.0 && u < 0.0) v += blob(p, vec2(221.0, 58.0), 8.0, 1.1 * g); if (u >= 0.0 && u < 1.0) v += blob(p, vec2(221.0, mix(58.0, 257.2, u * u)), 9.0, 1.15 * (1.0 - sm(0.9, 1.0, u))); float w = (t - 0.77) / 0.4; if (w > 0.0 && w < 1.0) { float sk = 24.0; if (k == sk && p.x > L && p.x < R) v += 0.7 * (1.0 - w) * exp(-pow(abs(p.x - 221.0) - 120.0 * w, 2.0) / 120.0); if (k == sk - 1.0 && w < 0.4 && abs(abs(p.x - 221.0) - 9.0) < 4.0) v += 0.7; } } { // drop 3: forms at its nozzle, falls gathering speed, lands on the surface, ripples out float u = (t - 0.75) / 0.32; float g = (t - 0.63) / 0.12; if (g > 0.0 && u < 0.0) v += blob(p, vec2(137.0, 58.0), 8.0, 1.1 * g); if (u >= 0.0 && u < 1.0) v += blob(p, vec2(137.0, mix(58.0, 229.0, u * u)), 9.0, 1.15 * (1.0 - sm(0.9, 1.0, u))); float w = (t - 1.07) / 0.4; if (w > 0.0 && w < 1.0) { float sk = 21.0; if (k == sk && p.x > L && p.x < R) v += 0.7 * (1.0 - w) * exp(-pow(abs(p.x - 137.0) - 120.0 * w, 2.0) / 120.0); if (k == sk - 1.0 && w < 0.4 && abs(abs(p.x - 137.0) - 9.0) < 4.0) v += 0.7; } } { // drop 4: forms at its nozzle, falls gathering speed, lands on the surface, ripples out float u = (t - 1.05) / 0.32; float g = (t - 0.93) / 0.12; if (g > 0.0 && u < 0.0) v += blob(p, vec2(200.0, 58.0), 8.0, 1.1 * g); if (u >= 0.0 && u < 1.0) v += blob(p, vec2(200.0, mix(58.0, 200.8, u * u)), 9.0, 1.15 * (1.0 - sm(0.9, 1.0, u))); float w = (t - 1.37) / 0.4; if (w > 0.0 && w < 1.0) { float sk = 18.0; if (k == sk && p.x > L && p.x < R) v += 0.7 * (1.0 - w) * exp(-pow(abs(p.x - 200.0) - 120.0 * w, 2.0) / 120.0); if (k == sk - 1.0 && w < 0.4 && abs(abs(p.x - 200.0) - 9.0) < 4.0) v += 0.7; } } { // drop 5: forms at its nozzle, falls gathering speed, lands on the surface, ripples out float u = (t - 1.35) / 0.32; float g = (t - 1.23) / 0.12; if (g > 0.0 && u < 0.0) v += blob(p, vec2(242.0, 58.0), 8.0, 1.1 * g); if (u >= 0.0 && u < 1.0) v += blob(p, vec2(242.0, mix(58.0, 172.6, u * u)), 9.0, 1.15 * (1.0 - sm(0.9, 1.0, u))); float w = (t - 1.67) / 0.4; if (w > 0.0 && w < 1.0) { float sk = 15.0; if (k == sk && p.x > L && p.x < R) v += 0.7 * (1.0 - w) * exp(-pow(abs(p.x - 242.0) - 120.0 * w, 2.0) / 120.0); if (k == sk - 1.0 && w < 0.4 && abs(abs(p.x - 242.0) - 9.0) < 4.0) v += 0.7; } } { // drop 6: forms at its nozzle, falls gathering speed, lands on the surface, ripples out float u = (t - 1.65) / 0.32; float g = (t - 1.53) / 0.12; if (g > 0.0 && u < 0.0) v += blob(p, vec2(158.0, 58.0), 8.0, 1.1 * g); if (u >= 0.0 && u < 1.0) v += blob(p, vec2(158.0, mix(58.0, 144.4, u * u)), 9.0, 1.15 * (1.0 - sm(0.9, 1.0, u))); float w = (t - 1.97) / 0.4; if (w > 0.0 && w < 1.0) { float sk = 12.0; if (k == sk && p.x > L && p.x < R) v += 0.7 * (1.0 - w) * exp(-pow(abs(p.x - 158.0) - 120.0 * w, 2.0) / 120.0); if (k == sk - 1.0 && w < 0.4 && abs(abs(p.x - 158.0) - 9.0) < 4.0) v += 0.7; } } // the outlet and the pipe: narrow walls, and the flow along them once the outlet is open float wl = 0.42; if (c == 27.0 && k > 32.0 && k <= 41.0) v += wl; if (c == 29.0 && k > 32.0 && k <= 39.0) v += wl; if (k == 39.0 && c > 29.0 && c < 62.0) v += wl; if (k == 41.0 && c >= 27.0 && c < 62.0) v += wl; float sd = -1.0; // how far along the flow's path this cell is if (c == 28.0 && k > 32.0 && k <= 40.0) sd = (k - 32.0) * 9.4; if (k == 40.0 && c > 28.0 && c < 62.0) sd = 75.2 + (c - 28.0) * 7.0; if (sd >= 0.0) { float at = sd / 1566.0; float on = step(2.3 + at, t) * step(t, 3.4 + at); v += on * (fract((sd - 900.0 * t) / 28.0) < 0.55 ? 1.05 : 0.55); } // the meter: a slot on top, a body with vents, its intake and a light that is on while it prints float printing = step(2.5, t) * step(t, 3.6); if (c >= 62.0 && c <= 88.0 && k >= 38.0 && k <= 42.0) { if (k == 38.0) v += c >= 66.0 && c <= 85.0 ? 0.2 : 1.0; else if (k == 39.0 || k == 42.0) v += 0.7; else if (c >= 86.0 && c < 88.0) v += 0.42 + 0.68 * printing; else if (c == 62.0) v += 0.42 + 0.63 * step(2.5, t) * step(t, 3.6); else v += mod(c, 2.0) < 0.5 ? 0.7 : 0.3; } // the slip: last month's lifts off and fades at the month's end; the new one comes out of the slot a row at a time if (c >= 67.0 && c <= 84.0 && k < 38.0) { float top = 13.0, a = 1.0, rows = 25.0; if (t >= 2.12 && t < 2.5) { float u = clamp((t - 2.12) / 0.28, 0.0, 1.0); top -= floor(2.0 * ease(u) + 0.5); a = 1.0 - ease(u); } else if (t >= 2.5) { rows = floor(25.0 * ease((t - 2.5) / 1.1) + 0.001); top = 38.0 - rows; } float r = k - top; if (r >= 0.0 && r < rows) v += a * slip(r, c - 67.0); } // the month: 30 days, each lit as it passes; day 30 the month's end; cleared left to right for the new month if (k == 45.0 && c >= 14.0 && c < 44.0) { float d = c - 13.0; float lit = step(0.15 + 0.065 * d, t) * (1.0 - step(3.62 + 0.006 * d, t)); v += 0.2 + lit * (d > 29.5 ? 0.9 : 0.55); } return v; }
Each run adds its usage to the month as it ends. On the last day the month settles into one invoice, line by line, with the total at the bottom.

How usage adds up

A run uses two things: sandbox minutes, from the moment the sandbox is ready until it is wiped, and model tokens, for every step the agent takes. Both are counted when the run ends and added to the month.

Routing keeps the token part small. Most steps go to a small model, and only the hard ones reach the large one. See Model routing.

Plans

HobbyTeamEnterprise
Price$0$40 per seat a monthCustom
Runs100 a monthUnlimitedUnlimited
Repos1UnlimitedUnlimited
Concurrent runs120Set by contract
Sandbox timeout1 hour24 hours24 hours
Trace history7 days90 days1 year

Spend limits

Set a monthly cap per workspace in Settings → Billing. When usage reaches it, new runs wait in the queue instead of starting, and you get an email at 80% and 100%. Runs already in progress always finish.

Tip

Put a lower cap on one repo with rate limits. A busy repo then can't use up the whole team's month.

Was this page helpful?

Open the dashboard