Drive Yard Plot from your own code
Everything the page does is available over HTTP. A natural use is a plan kept in a file - a CSV of placements from the page, or one exported from a CAD or garden tool - where a script re-measures it whenever it changes and asks for a re-review only when the planting has moved on.
One thing to be clear about first: the model never measures. The caller computes every distance, the light at each trunk, the findings and the totals, and sends them as facts with the plan. The model returns an assessment and concrete moves; the caller replays each move against its own checks before showing it. The page's own checker and replayer ship as plain scripts you can load in node — see the end of the examples.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"yard-plot"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object: never wrap it as {"input": {…}} — that returns 200, warns unknown field 'input', and hides every field from the model.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: reviews are metered and need a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field comes first
One system prompt serves two tasks. task picks the contract; anything else is read as review and the reply says so in notes.
| task | When | Extra fields |
|---|---|---|
review | The first look at a plan, or a fresh look with no history. | None. |
rereview | After the person accepted or declined moves from a review, or edited the plan since. | previous and changes; the reply adds one previous entry per earlier move. |
Every run body is a JSON object of string fields (the declared input schema; task, title, yard, items, checks and catalog are required, and /estimate warns when one is missing). Coordinates are feet from the lot's top-left corner, x east, y south.
| Field | What it holds |
|---|---|
task | "review" or "rereview". |
title | The plan's name. |
context | Optional. What the person wants from the yard: privacy, shade, flowers, upkeep, timing. The page cuts the middle past 1,500 characters. |
yard | JSON string: {"w":70,"h":45,"units":"ft","origin":"...","zone":6,"deer":"low|moderate|high","grid":1}. |
items | JSON string: structures {"id":"H1","kind":"house|patio|path|shed|bed|utility|shade|screen","x","y","w","h","level"?,"label"?} with (x, y) the top-left corner, and plants {"id":"P1","sp":"green-giant","x","y","label"?} with (x, y) the trunk. At most 120 items are sent; past that every structure and every plant with a finding goes, and clipped says how many plants were summarised by species. |
checks | JSON string: {"violations":[{"key":"foundation:P8|H1","rule","sev":"error|warn|info","items":[...],"msg"}],"totals":{...}}, computed by the caller. |
catalog | JSON string: the species the reviewer may use, [{"key","name","cat","w","h","sun":["full","part","shade"],"deer","ev","zones":[min,max],"spacing"?}]. Widths and heights are mature, in feet. |
clipped | Optional JSON string: what was left out. |
changes | rereview only. JSON string: [{"type":"moved|added|removed|species_changed|resized|...","id",...}]. |
previous | rereview only. JSON string: {"headline","moves":[{"ref":1,"action","item","x","y","with","species","reason","you":"accepted|declined|open"}]}. |
The reply text is one JSON object (the same envelope for both tasks):
{
"task": "review | rereview", "title": "...", "headline": "...",
"assessment": [{ "area": "spacing|foundation|boundary|sun|hardiness|deer|privacy|resilience|access|utilities|style|maintenance|phasing", "verdict": "good|fix|watch", "note", "items": ["P3"] }],
"moves": [{ "action": "move|swap|replace|remove|add", "item": "P3" or null, "x", "y", "with", "species", "reason", "addresses": ["foundation:P3|H1"] }],
"phasing": [{ "phase": "Year 1|Year 2|Year 3+", "items": [...], "why" }],
"next_checks": [...], "notes": [...],
"previous": [{ "ref": 1, "status": "done|still-needed|dropped|superseded", "note" }]
}
A move's x, y are the new trunk for a plant and the new top-left corner for a structure. Replay each move on a copy of the plan and re-run your checks before trusting it: the page marks a move that would add a finding as "would break".
Worked examples
These are the bundled examples on the page: the exact run body the page builds and a reply produced against the shipped prompt (the page replays the same replies for free).
review: a backyard privacy screen of seven arborvitae, high deer pressure
Input (the catalogue string is the same 38 species in every example and is not repeated here):
{
"task": "review",
"title": "Back garden - screen the new neighbours",
"context": "Two-storey house, north side of the lot. New houses behind us look straight onto the patio. We want privacy within about 3 years, low upkeep, and we have a lot of deer.",
"yard": "(a JSON string - decoded below)",
"items": "(a JSON string - decoded below)",
"checks": "(a JSON string - decoded below)",
"catalog": "(a JSON string: the 38-species catalogue, the same in every run - see the field table)"
}
yard, decoded:
{
"w": 70,
"h": 45,
"units": "ft",
"origin": "top-left corner; x grows right (east), y grows down (south)",
"zone": 6,
"deer": "high",
"grid": 1
}
items, decoded:
[
{
"id": "H1",
"kind": "house",
"x": 10,
"y": 0,
"w": 40,
"h": 18
},
{
"id": "PT1",
"kind": "patio",
"x": 20,
"y": 18,
"w": 16,
"h": 12
},
{
"id": "PA1",
"kind": "path",
"x": 34,
"y": 30,
"w": 2.5,
"h": 9,
"label": "Stepping stones"
},
{
"id": "SH1",
"kind": "shed",
"x": 56,
"y": 33,
"w": 10,
"h": 8
},
{
"id": "U1",
"kind": "utility",
"x": 50,
"y": 18,
"w": 3,
"h": 3,
"label": "AC unit"
},
{
"id": "SC1",
"kind": "screen",
"x": 0,
"y": 44,
"w": 70,
"h": 1,
"label": "Back fence view"
},
{
"id": "B1",
"kind": "bed",
"x": 4,
"y": 18,
"w": 14,
"h": 5
},
{
"id": "P1",
"sp": "green-giant",
"x": 4,
"y": 41
},
{
"id": "P2",
"sp": "green-giant",
"x": 10,
"y": 41
},
{
"id": "P3",
"sp": "green-giant",
"x": 16,
"y": 41
},
{
"id": "P4",
"sp": "green-giant",
"x": 22,
"y": 41
},
{
"id": "P5",
"sp": "green-giant",
"x": 28,
"y": 41
},
{
"id": "P6",
"sp": "green-giant",
"x": 40,
"y": 41
},
{
"id": "P7",
"sp": "green-giant",
"x": 46,
"y": 41
},
{
"id": "P8",
"sp": "boxwood",
"x": 8,
"y": 20
},
{
"id": "P9",
"sp": "boxwood",
"x": 13,
"y": 20
},
{
"id": "P10",
"sp": "panicle-hydrangea",
"x": 54,
"y": 22.5
},
{
"id": "P11",
"sp": "japanese-maple",
"x": 8,
"y": 31,
"label": "Specimen maple"
},
{
"id": "P12",
"sp": "hosta",
"x": 40,
"y": 24
},
{
"id": "P13",
"sp": "lavender",
"x": 18,
"y": 21
}
]
checks, decoded:
{
"violations": [
{
"key": "foundation:P8|H1",
"rule": "foundation",
"sev": "error",
"items": [
"P8",
"H1"
],
"msg": "P8 Boxwood grows 4 ft wide; its canopy comes within 0.8 ft of the wall. Put its centre at least 3 ft from the house."
},
{
"key": "foundation:P9|H1",
"rule": "foundation",
"sev": "error",
"items": [
"P9",
"H1"
],
"msg": "P9 Boxwood grows 4 ft wide; its canopy touches the house wall. Put its centre at least 3 ft from the house."
},
{
"key": "utility:P10|U1",
"rule": "utility",
"sev": "error",
"items": [
"P10",
"U1"
],
"msg": "P10 Hydrangea paniculata at maturity grows over U1 AC unit; keep 3 ft clear for airflow and service (centre 6.5 ft away)."
},
{
"key": "boundary:P1",
"rule": "boundary",
"sev": "warn",
"items": [
"P1"
],
"msg": "P1 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "boundary:P2",
"rule": "boundary",
"sev": "warn",
"items": [
"P2"
],
"msg": "P2 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "boundary:P3",
"rule": "boundary",
"sev": "warn",
"items": [
"P3"
],
"msg": "P3 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "boundary:P4",
"rule": "boundary",
"sev": "warn",
"items": [
"P4"
],
"msg": "P4 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "boundary:P5",
"rule": "boundary",
"sev": "warn",
"items": [
"P5"
],
"msg": "P5 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "boundary:P6",
"rule": "boundary",
"sev": "warn",
"items": [
"P6"
],
"msg": "P6 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "boundary:P7",
"rule": "boundary",
"sev": "warn",
"items": [
"P7"
],
"msg": "P7 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "crowding:P1|P11",
"rule": "crowding",
"sev": "warn",
"items": [
"P1",
"P11"
],
"msg": "P1 Arborvitae 'Green Giant' and P11 Specimen maple are 10.8 ft apart; at maturity they need 13.2 ft."
},
{
"key": "crowding:P2|P11",
"rule": "crowding",
"sev": "warn",
"items": [
"P2",
"P11"
],
"msg": "P2 Arborvitae 'Green Giant' and P11 Specimen maple are 10.2 ft apart; at maturity they need 13.2 ft."
},
{
"key": "crowding:P3|P11",
"rule": "crowding",
"sev": "warn",
"items": [
"P3",
"P11"
],
"msg": "P3
... (shortened here)
Reply:
{
"task": "review",
"title": "Back garden - screen the new neighbours",
"headline": "The bones are right - an evergreen line along the back fence - but seven Green Giants 4 ft off a 45-ft-deep lot are the wrong plant for deer country: they are deer candy, they will spread 16 ft into the neighbours and over your stepping stones, and one pest takes the whole screen. Fix the three foundation and AC errors now, then start turning the row into a mixed, deer-resistant screen.",
"assessment": [
{
"area": "foundation",
"verdict": "fix",
"note": "P8 and P9 Boxwood sit 2 ft from the house wall; at 4 ft wide their canopy touches the siding and traps moisture. Drop them 1 ft south to y 21 so the centres are 3 ft out, still inside bed B1.",
"items": [
"P8",
"P9",
"H1",
"B1"
]
},
{
"area": "utilities",
"verdict": "fix",
"note": "P10 Hydrangea paniculata is 1.5 ft from the U1 AC unit and will grow 7 ft wide over it, choking the airflow and blocking service. Move it to x 58, y 26, about 7 ft clear of U1.",
"items": [
"P10",
"U1"
]
},
{
"area": "deer",
"verdict": "fix",
"note": "With high deer pressure, P1-P7 Arborvitae 'Green Giant' are the first thing deer will strip in winter - they browse every arborvitae up to about 6 ft, which is exactly the height you need for privacy at the patio. P12 Hosta is eaten to the stem as well. Every new screen plant here should be deer-resistant: Nellie Stevens holly, skip laurel or Cryptomeria.",
"items": [
"P1",
"P2",
"P3",
"P4",
"P5",
"P6",
"P7",
"P12"
]
},
{
"area": "resilience",
"verdict": "fix",
"note": "Seven Green Giants in one row is a monoculture: bagworm or a bad winter of deer browse takes the whole screen at once. Mix in 2-3 species. The row is planted on 6 ft centres so a different species cannot be slotted between them today; start with the Nellie Stevens holly east of P7, and as any Green Giant fails, replace it with holly or skip laurel rather than another arborvitae.",
"items": [
"P1",
"P2",
"P3",
"P4",
"P5",
"P6",
"P7"
]
},
{
"area": "boundary",
"verdict": "watch",
"note": "P1-P7 are 4 ft off the back fence but spread 16 ft, so each will hang 4 ft into the new neighbours' yards and P5 and P6 will grow 1.7 ft and 4 ft over the PA1 stepping stones. Expect to shear the back face every year; check the local rules on boundary hedges before you count on growing past the line.",
"items": [
"P1",
"P2",
"P3",
"P4",
"P5",
"P6",
"P7",
"PA1"
]
},
{
"area": "privacy",
"verdict": "watch",
"note": "SC1 is 76% covered at maturity, with a 16.8 ft gap from x 53 to 70 behind the SH1 shed. Green Giant puts on 3 ft a year, so the planted stretch should block the patio view in about 3 years if the deer leave it alone; the shed covers the low part of the gap and a holly at x 50, y 30 screens the sightline from the east end over its roof.",
"items": [
"SC1",
"SH1"
]
},
{
"area": "sun",
"verdict": "watch",
"note": "P11 Specimen maple wants part shade and will scorch here in full afternoon sun; it is also 10-13 ft from P1-P3, which it needs 13.2 ft from. Pull it north to x 8, y 27 to clear the arborvitae, mulch it well and water it through the first two summers.",
"items": [
"P11"
]
},
{
"area": "access",
"verdict": "watch",
"note": "PA1 stepping stones are only 2.5 ft wide; widen them to 3 ft when you set them, and keep the arborvitae sheared back where P5 and P6 lean over.",
"items": [
"PA1"
]
}
],
"moves": [
{
"action": "move",
"item": "P8",
"x": 8,
"y": 21,
"with": null,
"species": null,
"reason": "Puts the boxwood centre 3 ft from the house so its mature 4 ft canopy clears the wall by 1 ft.",
"addresses": [
"foundation:P8|H1"
]
},
{
"action": "move",
"item": "P9",
"x": 13,
"y": 21,
"with": null,
"species": null,
"reason": "Same fix as P8: 3 ft from the wall keeps the canopy off the siding and leaves room to paint and inspect.",
"addresses": [
"foundation:P9|H
... (shortened here; the page shows it whole)
review: a narrow shady side yard with hemlocks and a birch being kept
Input:
{
"task": "review",
"title": "North side yard under the birch",
"context": "Narrow side yard between our house and the neighbour's fence. A big river birch we are keeping shades the far end. Zone 5. I would like flowers and an evergreen screen along the fence.",
"yard": "(a JSON string - decoded below)",
"items": "(a JSON string - decoded below)",
"checks": "(a JSON string - decoded below)",
"catalog": "(a JSON string: the 38-species catalogue, the same in every run - see the field table)"
}
yard, decoded:
{
"w": 34,
"h": 60,
"units": "ft",
"origin": "top-left corner; x grows right (east), y grows down (south)",
"zone": 5,
"deer": "moderate",
"grid": 1
}
items, decoded:
[
{
"id": "H1",
"kind": "house",
"x": 0,
"y": 0,
"w": 12,
"h": 60
},
{
"id": "SZ1",
"kind": "shade",
"x": 12,
"y": 0,
"w": 22,
"h": 24,
"level": "full",
"label": "House shadow"
},
{
"id": "PA1",
"kind": "path",
"x": 14.5,
"y": 4,
"w": 3,
"h": 52,
"label": "Gravel path"
},
{
"id": "U1",
"kind": "utility",
"x": 12,
"y": 30,
"w": 2,
"h": 2,
"label": "Gas meter"
},
{
"id": "SC1",
"kind": "screen",
"x": 33,
"y": 4,
"w": 1,
"h": 50,
"label": "Fence line"
},
{
"id": "B1",
"kind": "bed",
"x": 19,
"y": 6,
"w": 12,
"h": 46
},
{
"id": "P1",
"sp": "river-birch",
"x": 26,
"y": 53,
"label": "Existing birch"
},
{
"id": "P2",
"sp": "hemlock",
"x": 29,
"y": 10
},
{
"id": "P3",
"sp": "hemlock",
"x": 29,
"y": 17
},
{
"id": "P4",
"sp": "hemlock",
"x": 29,
"y": 24
},
{
"id": "P5",
"sp": "rose",
"x": 22,
"y": 8
},
{
"id": "P6",
"sp": "lavender",
"x": 21,
"y": 28
},
{
"id": "P7",
"sp": "azalea",
"x": 21,
"y": 38
},
{
"id": "P8",
"sp": "hosta",
"x": 21,
"y": 44
},
{
"id": "P9",
"sp": "fern",
"x": 25,
"y": 46
},
{
"id": "P10",
"sp": "astilbe",
"x": 20,
"y": 49
},
{
"id": "P11",
"sp": "oakleaf-hydrangea",
"x": 25,
"y": 40
}
]
checks, decoded:
{
"violations": [
{
"key": "foundation:P1|H1",
"rule": "foundation",
"sev": "error",
"items": [
"P1",
"H1"
],
"msg": "P1 Existing birch grows 40 ft wide; its canopy hits the house wall by 6 ft. Put its centre at least 21 ft from the house."
},
{
"key": "hardiness:azalea",
"rule": "hardiness",
"sev": "error",
"items": [
"P7"
],
"msg": "Azalea (P7) is hardy in zones 6-9; this yard is zone 5."
},
{
"key": "boundary:P1",
"rule": "boundary",
"sev": "warn",
"items": [
"P1"
],
"msg": "P1 Existing birch spreads 40 ft; it will overhang the lot line by 13 ft (centre 7 ft from the line, needs 20)."
},
{
"key": "boundary:P2",
"rule": "boundary",
"sev": "warn",
"items": [
"P2"
],
"msg": "P2 Canadian hemlock spreads 25 ft; it will overhang the lot line by 7.5 ft (centre 5 ft from the line, needs 12.5)."
},
{
"key": "boundary:P3",
"rule": "boundary",
"sev": "warn",
"items": [
"P3"
],
"msg": "P3 Canadian hemlock spreads 25 ft; it will overhang the lot line by 7.5 ft (centre 5 ft from the line, needs 12.5)."
},
{
"key": "boundary:P4",
"rule": "boundary",
"sev": "warn",
"items": [
"P4"
],
"msg": "P4 Canadian hemlock spreads 25 ft; it will overhang the lot line by 7.5 ft (centre 5 ft from the line, needs 12.5)."
},
{
"key": "crowding:P2|P5",
"rule": "crowding",
"sev": "warn",
"items": [
"P2",
"P5"
],
"msg": "P2 Canadian hemlock and P5 Shrub rose are 7.3 ft apart; at maturity they need 12.3 ft."
},
{
"key": "crowding:P3|P5",
"rule": "crowding",
"sev": "warn",
"items": [
"P3",
"P5"
],
"msg": "P3 Canadian hemlock and P5 Shrub rose are 11.4 ft apart; at maturity they need 12.3 ft."
},
{
"key": "crowding:P4|P6",
"rule": "crowding",
"sev": "warn",
"items": [
"P4",
"P6"
],
"msg": "P4 Canadian hemlock and P6 Lavender are 8.9 ft apart; at maturity they need 11.7 ft."
},
{
"key": "crowding:P7|P11",
"rule": "crowding",
"sev": "warn",
"items": [
"P7",
"P11"
],
"msg": "P7 Azalea and P11 Oakleaf hydrangea are 4.5 ft apart; at maturity they need 4.7 ft."
},
{
"key": "intrusion:P1|PA1",
"rule": "intrusion",
"sev": "warn",
"items": [
"P1",
"PA1"
],
"msg": "P1 Existing birch will spread 11.5 ft over PA1 Gravel path."
},
{
"key": "screen:SC1",
"rule": "screen",
"sev": "warn",
"items": [
"SC1",
"P2",
"P3",
"P4"
],
"msg": "SC1 Fence line is 63% covered by evergreen canopy at maturity; gap 18.3 ft at 35.7-54 (P1 is bare in winter and not counted)."
},
{
"key": "sun:P5",
"rule": "sun",
"sev": "warn",
"items": [
"P5",
"SZ1",
"P2",
"P3"
],
"msg": "P5 Shrub rose wants full sun but sits in full shade (under SZ1, P2, P3)."
},
{
"key": "sun:P6",
"rule": "sun",
"sev": "warn",
"items": [
"P6",
... (shortened here)
Reply:
{
"task": "review",
"title": "North side yard under the birch",
"headline": "Hemlock is the right screen for a fence in full house shadow, but the flowers are in the wrong light: the rose sits in deep shade, the lavender under P4, and the azalea will not survive zone 5. Swap those three for shade-loving bloomers, keep the birch, and shear the hemlocks as a hedge so they do not swallow the bed.",
"assessment": [
{
"area": "hardiness",
"verdict": "fix",
"note": "P7 Azalea is hardy only to zone 6 and this yard is zone 5; it will die back or die outright in a hard winter, and deer like it too. Replace it with astilbe, which flowers in the birch's part shade and also clears P11 Oakleaf hydrangea.",
"items": [
"P7",
"P11"
]
},
{
"area": "foundation",
"verdict": "watch",
"note": "P1 Existing birch is 14 ft from the house and its 40 ft canopy reaches 6 ft past the wall, and it spreads 11.5 ft over the PA1 gravel path and 13 ft over the fence. It is a mature tree you are keeping, so it can stand: have an arborist lift and thin the limbs over the roof and gutters and keep branches 6-10 ft off the siding.",
"items": [
"P1",
"H1",
"PA1"
]
},
{
"area": "sun",
"verdict": "fix",
"note": "P5 Shrub rose sits inside SZ1, the house shadow, and under P2 and P3; it needs 6+ hours and will get almost none. P6 Lavender is under the P4 hemlock canopy. Replace the rose with bleeding heart and move the lavender to x 20, y 33, the one spot in the bed that is past the house shadow and clear of both P4 and the birch.",
"items": [
"P5",
"P6",
"SZ1",
"P4"
]
},
{
"area": "privacy",
"verdict": "watch",
"note": "P2-P4 cover SC1 from y 4 to about 36; the 18 ft gap at the far end is under P1, which is bare in winter. In full shade nothing else in the catalogue makes a 6 ft evergreen screen, so accept a winter view at that end or add a fence panel there.",
"items": [
"SC1",
"P2",
"P3",
"P4",
"P1"
]
},
{
"area": "boundary",
"verdict": "watch",
"note": "P2-P4 Canadian hemlock grow 25 ft wide naturally but sit 5 ft from the fence. Hemlock takes shearing well: keep them clipped as a 6-8 ft wide hedge from year 2 and they will stay off the neighbour's side and leave room for the bed. Check the local rules on boundary hedges.",
"items": [
"P2",
"P3",
"P4"
]
},
{
"area": "deer",
"verdict": "watch",
"note": "With moderate deer, P2-P4 hemlock and P8 Hosta will be browsed some winters. Watch the lower branches of the hemlocks; the bleeding heart and astilbe going in are deer-resistant.",
"items": [
"P8"
]
},
{
"area": "maintenance",
"verdict": "good",
"note": "The shade end already has the right plants: P9 Christmas fern, P10 Astilbe and P11 Oakleaf hydrangea all want part shade and suit the birch.",
"items": [
"P9",
"P10",
"P11"
]
}
],
"moves": [
{
"action": "replace",
"item": "P7",
"x": null,
"y": null,
"with": null,
"species": "astilbe",
"reason": "Astilbe is hardy in zone 5, flowers in part shade, deer leave it alone, and at 2 ft wide it clears the oakleaf hydrangea.",
"addresses": [
"hardiness:azalea",
"crowding:P7|P11"
]
},
{
"action": "move",
"item": "P6",
"x": 20,
"y": 33,
"with": null,
"species": null,
"reason": "Here the lavender is past the house shadow, 12.7 ft from P4 and 21 ft from the birch, so it gets full sun and no hemlock canopy over it.",
"addresses": [
"sun:P6",
"crowding:P4|P6"
]
},
{
"action": "replace",
"item": "P5",
"x": null,
"y": null,
"with": null,
"species": "bleeding-heart",
"reason": "Bleeding heart flowers in full shade where the rose never would, and deer leave it alone. It will still need room as the hemlocks fill in, so keep those sheared.",
"addresses": [
"sun:P5"
]
}
],
"phasing": [
{
"phase": "Year 1",
"items": [
"P2",
"P3",
"P4",
"P1"
],
"why": "Get the hemlock screen established and have the birch pruned off the house this fall."
},
{
"phase": "Year 2",
"item
... (shortened here; the page shows it whole)
review: a formal front foundation bed
Input:
{
"task": "review",
"title": "Front of the house - foundation planting",
"context": "Colonial house facing south onto the street. We want something formal and evergreen along the front, a tree for some shade on the lawn, and a clear walk to the door.",
"yard": "(a JSON string - decoded below)",
"items": "(a JSON string - decoded below)",
"checks": "(a JSON string - decoded below)",
"catalog": "(a JSON string: the 38-species catalogue, the same in every run - see the field table)"
}
yard, decoded:
{
"w": 50,
"h": 30,
"units": "ft",
"origin": "top-left corner; x grows right (east), y grows down (south)",
"zone": 7,
"deer": "low",
"grid": 0.5
}
items, decoded:
[
{
"id": "H1",
"kind": "house",
"x": 5,
"y": 0,
"w": 40,
"h": 8
},
{
"id": "B1",
"kind": "bed",
"x": 5,
"y": 8,
"w": 18,
"h": 5
},
{
"id": "B2",
"kind": "bed",
"x": 27,
"y": 8,
"w": 18,
"h": 5
},
{
"id": "PA1",
"kind": "path",
"x": 23,
"y": 8,
"w": 4,
"h": 22,
"label": "Front walk"
},
{
"id": "U1",
"kind": "utility",
"x": 42,
"y": 8,
"w": 2.5,
"h": 2.5,
"label": "Electric meter"
},
{
"id": "P1",
"sp": "leyland",
"x": 7,
"y": 11
},
{
"id": "P2",
"sp": "boxwood",
"x": 11,
"y": 11
},
{
"id": "P3",
"sp": "boxwood",
"x": 13,
"y": 11
},
{
"id": "P4",
"sp": "boxwood",
"x": 15,
"y": 11
},
{
"id": "P5",
"sp": "boxwood",
"x": 17,
"y": 11
},
{
"id": "P6",
"sp": "boxwood",
"x": 19,
"y": 11
},
{
"id": "P7",
"sp": "southern-magnolia",
"x": 38,
"y": 18
},
{
"id": "P8",
"sp": "japanese-maple",
"x": 12,
"y": 22
},
{
"id": "P9",
"sp": "boxwood",
"x": 30,
"y": 11
},
{
"id": "P10",
"sp": "boxwood",
"x": 33,
"y": 11
},
{
"id": "P11",
"sp": "rhododendron",
"x": 40,
"y": 11
}
]
checks, decoded:
{
"violations": [
{
"key": "foundation:P11|H1",
"rule": "foundation",
"sev": "error",
"items": [
"P11",
"H1"
],
"msg": "P11 Rhododendron grows 6 ft wide; its canopy touches the house wall. Put its centre at least 4 ft from the house."
},
{
"key": "foundation:P1|H1",
"rule": "foundation",
"sev": "error",
"items": [
"P1",
"H1"
],
"msg": "P1 Leyland cypress grows 12 ft wide; its canopy hits the house wall by 3 ft. Put its centre at least 7 ft from the house."
},
{
"key": "foundation:P7|H1",
"rule": "foundation",
"sev": "error",
"items": [
"P7",
"H1"
],
"msg": "P7 Southern magnolia grows 30 ft wide; its canopy hits the house wall by 5 ft. Put its centre at least 16 ft from the house."
},
{
"key": "overlap:B2|U1",
"rule": "overlap",
"sev": "error",
"items": [
"B2",
"U1"
],
"msg": "B2 Planting bed and U1 Electric meter overlap by 6.3 sq ft."
},
{
"key": "utility:P11|U1",
"rule": "utility",
"sev": "error",
"items": [
"P11",
"U1"
],
"msg": "P11 Rhododendron at maturity grows over U1 Electric meter; keep 3 ft clear for airflow and service (centre 6 ft away)."
},
{
"key": "utility:P7|U1",
"rule": "utility",
"sev": "error",
"items": [
"P7",
"U1"
],
"msg": "P7 Southern magnolia at maturity grows over U1 Electric meter; keep 3 ft clear for airflow and service (centre 18 ft away)."
},
{
"key": "boundary:P7",
"rule": "boundary",
"sev": "warn",
"items": [
"P7"
],
"msg": "P7 Southern magnolia spreads 30 ft; it will overhang the lot line by 3 ft (centre 12 ft from the line, needs 15)."
},
{
"key": "crowding:P1|P2",
"rule": "crowding",
"sev": "warn",
"items": [
"P1",
"P2"
],
"msg": "P1 Leyland cypress and P2 Boxwood are 4 ft apart; at maturity they need 6.8 ft."
},
{
"key": "crowding:P1|P3",
"rule": "crowding",
"sev": "warn",
"items": [
"P1",
"P3"
],
"msg": "P1 Leyland cypress and P3 Boxwood are 6 ft apart; at maturity they need 6.8 ft."
},
{
"key": "intrusion:P7|PA1",
"rule": "intrusion",
"sev": "warn",
"items": [
"P7",
"PA1"
],
"msg": "P7 Southern magnolia will spread 4 ft over PA1 Front walk."
},
{
"key": "sun:P8",
"rule": "sun",
"sev": "warn",
"items": [
"P8"
],
"msg": "P8 Japanese maple wants part shade but sits in full sun."
}
],
"totals": {
"lot_sqft": 1500,
"house_sqft": 320,
"hardscape_sqft": 88,
"bed_sqft": 180,
"canopy_pct": 63,
"bed_planted_pct": 89,
"open_pct": 18,
"plants": 11,
"species": 5,
"by_species": {
"leyland": 1,
"boxwood": 7,
"southern-magnolia": 1,
"japanese-maple": 1,
"rhododendron": 1
},
"by_cat": {
"screen": 1,
"shrub": 8,
"tree": 2
},
"evergreen_pct": 91,
"screens": [],
"screen_pct": null,
"errors": 6,
"warnings": 5,
"notes": 0
}
}
Reply:
{
"task": "review",
"title": "Front of the house - foundation planting",
"headline": "A formal boxwood front suits a colonial, but two plants are far too big for a 30 ft front yard: the Southern magnolia grows 30 ft wide and cannot fit anywhere between the house and the street, and the Leyland cypress is a 60 ft screen tree jammed against the wall. Swap them for an American holly and a boxwood, pull the rhododendron off the wall and the meter, and the walk stays clear.",
"assessment": [
{
"area": "foundation",
"verdict": "fix",
"note": "P7 Southern magnolia needs its centre 16 ft from the house and 15 ft from the lot line, but the yard is only 22 ft deep in front of the house, so no spot works. It would also spread 4 ft over the PA1 front walk and grow over the U1 meter. An American holly at the same spot keeps the evergreen shade with half the spread.",
"items": [
"P7",
"H1",
"PA1",
"U1"
]
},
{
"area": "foundation",
"verdict": "fix",
"note": "P1 Leyland cypress grows 60 ft tall and 12 ft wide; at 3 ft from the wall its canopy would push into the house by 3 ft and crowd P2 and P3. A boxwood in its place finishes the clipped row in B1.",
"items": [
"P1",
"P2",
"P3"
]
},
{
"area": "utilities",
"verdict": "fix",
"note": "P11 Rhododendron is 3 ft from the wall and grows over the U1 electric meter. Moving it to x 37, y 14 keeps it 6 ft from the meter and still in the magnolia's or holly's shade, which it needs on a south-facing front. Bed B2 also runs 2.5 ft under the meter; drop B2 to start at y 10.5 so the meter stands on a clear service strip.",
"items": [
"P11",
"U1",
"B2"
]
},
{
"area": "style",
"verdict": "good",
"note": "The clipped boxwood rows P2-P6 and P9-P10 either side of the 4 ft PA1 walk are the right idiom for a colonial. With P1 as a boxwood the west row runs evenly from x 7 to 19; add matching boxwoods in B2 later so both sides mirror.",
"items": [
"P2",
"P3",
"P4",
"P5",
"P6",
"P9",
"P10",
"PA1"
]
},
{
"area": "sun",
"verdict": "watch",
"note": "P8 Japanese maple is your lawn tree but sits in full south sun, which scorches its leaves in a zone 7 July. It can stay: plant it in fall, mulch the root zone and water deeply through the first two summers. If the leaves still burn, move it into the holly's afternoon shade.",
"items": [
"P8"
]
},
{
"area": "utilities",
"verdict": "watch",
"note": "The holly at P7 will still reach within about 1 ft of U1 at full size; keep the side facing the meter pruned so the meter reader has a clear path.",
"items": [
"P7"
]
}
],
"moves": [
{
"action": "replace",
"item": "P7",
"x": null,
"y": null,
"with": null,
"species": "american-holly",
"reason": "A 15 ft wide American holly fits at this spot: it clears the house, the lot line and the walk, and it is an evergreen that suits a formal front.",
"addresses": [
"foundation:P7|H1",
"boundary:P7",
"intrusion:P7|PA1"
]
},
{
"action": "move",
"item": "P11",
"x": 37,
"y": 14,
"with": null,
"species": null,
"reason": "Puts the rhododendron 6 ft from the wall and 6 ft from the meter, and keeps it in the evergreen shade it needs.",
"addresses": [
"foundation:P11|H1",
"utility:P11|U1"
]
},
{
"action": "replace",
"item": "P1",
"x": null,
"y": null,
"with": null,
"species": "boxwood",
"reason": "A boxwood stays 4 ft wide and 3 ft from the wall, completing the formal row instead of a 60 ft cypress against the house.",
"addresses": [
"foundation:P1|H1",
"crowding:P1|P2",
"crowding:P1|P3"
]
},
{
"action": "move",
"item": "B2",
"x": 27,
"y": 10.5,
"with": null,
"species": null,
"reason": "Starting the bed 2.5 ft off the wall clears the electric meter and leaves a gravel service strip in front of it.",
"addresses": [
"overlap:B2|U1"
]
}
],
"phasing": [
{
"phase": "Year 1",
"items": [
"american-holly",
"P8",
"B2",
"P11"
],
"why": "Plant the holly and settle the mapl
... (shortened here; the page shows it whole)
rereview: the backyard after some moves were accepted, one declined and a holly added by hand
Input (previous lists each earlier move with what the person did, changes what moved since):
{
"task": "rereview",
"title": "Back garden - screen the new neighbours",
"context": "Two-storey house, north side of the lot. New houses behind us look straight onto the patio. We want privacy within about 3 years, low upkeep, and we have a lot of deer.",
"yard": "(a JSON string - decoded below)",
"items": "(a JSON string - decoded below)",
"checks": "(a JSON string - decoded below)",
"catalog": "(a JSON string: the 38-species catalogue, the same in every run - see the field table)",
"changes": "(a JSON string - decoded below)",
"previous": "(a JSON string - decoded below)"
}
yard, decoded:
{
"w": 70,
"h": 45,
"units": "ft",
"origin": "top-left corner; x grows right (east), y grows down (south)",
"zone": 6,
"deer": "high",
"grid": 1
}
items, decoded:
[
{
"id": "H1",
"kind": "house",
"x": 10,
"y": 0,
"w": 40,
"h": 18
},
{
"id": "PT1",
"kind": "patio",
"x": 20,
"y": 18,
"w": 16,
"h": 12
},
{
"id": "PA1",
"kind": "path",
"x": 34,
"y": 30,
"w": 2.5,
"h": 9,
"label": "Stepping stones"
},
{
"id": "SH1",
"kind": "shed",
"x": 56,
"y": 33,
"w": 10,
"h": 8
},
{
"id": "U1",
"kind": "utility",
"x": 50,
"y": 18,
"w": 3,
"h": 3,
"label": "AC unit"
},
{
"id": "SC1",
"kind": "screen",
"x": 0,
"y": 44,
"w": 70,
"h": 1,
"label": "Back fence view"
},
{
"id": "B1",
"kind": "bed",
"x": 4,
"y": 18,
"w": 14,
"h": 5
},
{
"id": "P1",
"sp": "green-giant",
"x": 4,
"y": 41
},
{
"id": "P2",
"sp": "green-giant",
"x": 10,
"y": 41
},
{
"id": "P3",
"sp": "green-giant",
"x": 16,
"y": 41
},
{
"id": "P4",
"sp": "green-giant",
"x": 22,
"y": 41
},
{
"id": "P5",
"sp": "green-giant",
"x": 28,
"y": 41
},
{
"id": "P6",
"sp": "green-giant",
"x": 40,
"y": 41
},
{
"id": "P7",
"sp": "green-giant",
"x": 46,
"y": 41
},
{
"id": "P8",
"sp": "boxwood",
"x": 8,
"y": 21
},
{
"id": "P9",
"sp": "boxwood",
"x": 13,
"y": 21
},
{
"id": "P10",
"sp": "panicle-hydrangea",
"x": 58,
"y": 26
},
{
"id": "P11",
"sp": "japanese-maple",
"x": 8,
"y": 27,
"label": "Specimen maple"
},
{
"id": "P12",
"sp": "hosta",
"x": 40,
"y": 24
},
{
"id": "P13",
"sp": "lavender",
"x": 18,
"y": 21
},
{
"id": "P14",
"sp": "nellie-stevens",
"x": 50,
"y": 30
},
{
"id": "P15",
"sp": "american-holly",
"x": 62,
"y": 12,
"label": "Added by hand"
}
]
checks, decoded:
{
"violations": [
{
"key": "boundary:P1",
"rule": "boundary",
"sev": "warn",
"items": [
"P1"
],
"msg": "P1 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "boundary:P2",
"rule": "boundary",
"sev": "warn",
"items": [
"P2"
],
"msg": "P2 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "boundary:P3",
"rule": "boundary",
"sev": "warn",
"items": [
"P3"
],
"msg": "P3 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "boundary:P4",
"rule": "boundary",
"sev": "warn",
"items": [
"P4"
],
"msg": "P4 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "boundary:P5",
"rule": "boundary",
"sev": "warn",
"items": [
"P5"
],
"msg": "P5 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "boundary:P6",
"rule": "boundary",
"sev": "warn",
"items": [
"P6"
],
"msg": "P6 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "boundary:P7",
"rule": "boundary",
"sev": "warn",
"items": [
"P7"
],
"msg": "P7 Arborvitae 'Green Giant' spreads 16 ft; it will overhang the lot line by 4 ft (centre 4 ft from the line, needs 8)."
},
{
"key": "deer:green-giant",
"rule": "deer",
"sev": "warn",
"items": [
"P1",
"P2",
"P3",
"P4",
"P5",
"P6",
"P7"
],
"msg": "Arborvitae 'Green Giant' (P1, P2, P3, P4, P5, P6, P7) is deer candy; with high deer pressure expect browsing."
},
{
"key": "deer:hosta",
"rule": "deer",
"sev": "warn",
"items": [
"P12"
],
"msg": "Hosta (P12) is deer candy; with high deer pressure expect browsing."
},
{
"key": "intrusion:P5|PA1",
"rule": "intrusion",
"sev": "warn",
"items": [
"P5",
"PA1"
],
"msg": "P5 Arborvitae 'Green Giant' will spread 1.7 ft over PA1 Stepping stones."
},
{
"key": "intrusion:P6|PA1",
"rule": "intrusion",
"sev": "warn",
"items": [
"P6",
"PA1"
],
"msg": "P6 Arborvitae 'Green Giant' will spread 4 ft over PA1 Stepping stones."
},
{
"key": "monoculture:green-giant",
"rule": "monoculture",
"sev": "warn",
"items": [
"P1",
"P2",
"P3",
"P4",
"P5",
"P6",
"P7"
],
"msg": "7 Arborvitae 'Green Giant' in one row; a single pest or disease can take the whole screen. Mix 2-3 compatible species."
},
{
"key": "path:PA1",
"rule": "path",
"sev": "warn",
"items": [
"PA1"
],
"msg": "PA1 Stepping stones is 2.5 ft wide; a garden
... (shortened here)
changes, decoded:
[
{
"type": "moved",
"id": "P8",
"from": [
8,
20
],
"to": [
8,
21
]
},
{
"type": "moved",
"id": "P9",
"from": [
13,
20
],
"to": [
13,
21
]
},
{
"type": "moved",
"id": "P10",
"from": [
54,
22.5
],
"to": [
58,
26
]
},
{
"type": "moved",
"id": "P11",
"from": [
8,
31
],
"to": [
8,
27
]
},
{
"type": "added",
"id": "P14",
"kind": "plant",
"species": "nellie-stevens",
"at": [
50,
30
]
},
{
"type": "added",
"id": "P15",
"kind": "plant",
"species": "american-holly",
"at": [
62,
12
]
}
]
previous, decoded:
{
"headline": "The bones are right - an evergreen line along the back fence - but seven Green Giants 4 ft off a 45-ft-deep lot are the wrong plant for deer country: they are deer candy, they will spread 16 ft into the neighbours and over your stepping stones, and one pest takes the whole screen. Fix the three foundation and AC errors now, then start turning the row into a mixed, deer-resistant screen.",
"moves": [
{
"ref": 1,
"action": "move",
"reason": "Puts the boxwood centre 3 ft from the house so its mature 4 ft canopy clears the wall by 1 ft.",
"you": "accepted",
"item": "P8",
"x": 8,
"y": 21
},
{
"ref": 2,
"action": "move",
"reason": "Same fix as P8: 3 ft from the wall keeps the canopy off the siding and leaves room to paint and inspect.",
"you": "accepted",
"item": "P9",
"x": 13,
"y": 21
},
{
"ref": 3,
"action": "move",
"reason": "At 7 ft wide the hydrangea needs its centre 6.5 ft from the AC unit; here it is about 7 ft clear and still in sun.",
"you": "accepted",
"item": "P10",
"x": 58,
"y": 26
},
{
"ref": 4,
"action": "add",
"reason": "A deer-resistant holly is the first non-arborvitae in the screen. It sits east of P7 where the neighbours look over the shed toward the patio, clear of the AC …",
"you": "accepted",
"species": "nellie-stevens",
"x": 50,
"y": 30
},
{
"ref": 5,
"action": "move",
"reason": "Gives the maple the 13.2 ft it needs from P1, P2 and P3; the boxwood and lavender in B1 are low enough to grow under it.",
"you": "accepted",
"item": "P11",
"x": 8,
"y": 27
},
{
"ref": 6,
"action": "replace",
"reason": "Hosta is deer candy and burns in full sun; catmint takes full sun, deer leave it alone and it flowers all summer beside the patio.",
"you": "declined",
"item": "P12",
"species": "catmint"
}
]
}
Reply:
{
"task": "rereview",
"title": "Back garden - screen the new neighbours",
"headline": "Good round: the wall and AC errors are gone, the maple is clear of the arborvitae and the screen now has its first deer-resistant holly. What is left is the Green Giant row itself, a long-term job, and the hosta, which can stay if it moves into the maple's shade.",
"assessment": [
{
"area": "style",
"verdict": "good",
"note": "The P15 American holly you added at x 62, y 12 is a good choice for deer country: evergreen, deer-resistant, hardy here, and it clears the house and the U1 AC unit. It sits on the north side, though, so it screens the east side of the house, not the view from the new houses behind the patio.",
"items": [
"P15",
"U1"
]
},
{
"area": "resilience",
"verdict": "watch",
"note": "P14 Nellie Stevens holly and P15 now give the yard two hollies next to the arborvitae, but the back row is still seven Green Giants on 6 ft centres. Keep replacing any that get browsed or fail with Nellie Stevens or skip laurel rather than another arborvitae.",
"items": [
"P14",
"P1",
"P2",
"P3",
"P4",
"P5",
"P6",
"P7"
]
},
{
"area": "deer",
"verdict": "fix",
"note": "P12 Hosta is still deer candy in full sun beside the patio. You kept it, so give it the light it wants: under the P11 maple it gets part shade and is closer to the house, where deer come less often. Spray or net it in spring if it gets eaten.",
"items": [
"P12",
"P11"
]
},
{
"area": "boundary",
"verdict": "watch",
"note": "P1-P7 still sit 4 ft off the fence with a 16 ft spread, so they will hang 4 ft over the line and P5 and P6 will lean over the PA1 stepping stones. Shear the back and path faces every year.",
"items": [
"P1",
"P2",
"P3",
"P4",
"P5",
"P6",
"P7",
"PA1"
]
},
{
"area": "privacy",
"verdict": "watch",
"note": "SC1 is still 76% covered with the gap behind the SH1 shed; P14 now screens the sightline over the shed roof toward the patio as it grows.",
"items": [
"SC1",
"SH1",
"P14"
]
},
{
"area": "sun",
"verdict": "watch",
"note": "P11 Specimen maple is still in full sun at its new spot; mulch it and water it through dry spells for the first two summers.",
"items": [
"P11"
]
}
],
"moves": [
{
"action": "move",
"item": "P12",
"x": 12,
"y": 29,
"with": null,
"species": null,
"reason": "You declined replacing the hosta, so move it 4.5 ft from the maple trunk into its part shade; it grows under the maple and stays clear of the arborvitae.",
"addresses": [
"sun:P12"
]
}
],
"phasing": [
{
"phase": "Year 1",
"items": [
"P14",
"P15"
],
"why": "Plant the two hollies this fall and water them through the first summer."
},
{
"phase": "Year 2",
"items": [
"PA1",
"P1",
"P2",
"P3",
"P4",
"P5",
"P6",
"P7"
],
"why": "Widen the stepping stones to 3 ft and replace any browsed Green Giant with holly or skip laurel."
},
{
"phase": "Year 3+",
"items": [
"P12",
"P13"
],
"why": "Move the hosta in early spring as it comes up, then fine-tune the perennials."
}
],
"next_checks": [
"Check the hosta spot under the maple for real shade in the afternoon in June.",
"Look for deer browse on the arborvitae this winter, below 6 ft.",
"Call 811 before digging the holly holes."
],
"notes": [
"The P15 holly is a 15 ft wide tree; leave it room on all sides and do not plant shrubs closer than about 8 ft."
],
"previous": [
{
"ref": 1,
"status": "done",
"note": "P8 now sits 3 ft from the house."
},
{
"ref": 2,
"status": "done",
"note": "P9 now sits 3 ft from the house."
},
{
"ref": 3,
"status": "done",
"note": "P10 is clear of the AC unit."
},
{
"ref": 4,
"status": "done",
"note": "P14 Nellie Stevens holly is in, the first non-arborvitae in the screen."
},
{
"ref": 5,
"status": "done",
"note": "P11 is clear of P1-P3."
},
{
"ref": 6,
"status": "superseded",
"note": "You kept the hosta,
... (shortened here; the page shows it whole)
The page measures and replays; the API does not
Over the API you get the review as JSON. The measurements in checks are yours to compute, and the page's own code is plain scripts you can load in node after global.window = {}: yardkit.js (YardKit.compute(plan) for the findings and totals, YardKit.buildInput(computed, {task}) for this exact run body, YardKit.diff(before, after) for a re-review's changes) and review.js (Review.normalize(reply, plan), Review.reconcile(result, computed, statuses) to replay every move against the checks, and Review.compact(result, statuses) for the next previous).
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://yard-plot.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://yard-plot.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// yard-plot.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://yard-plot.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://yard-plot.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// YardPlot.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class YardPlot {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://yard-plot.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://yard-plot.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://yard-plot.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://yard-plot.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"yard-plot"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "yard-plot"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "yard-plot" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "yard-plot"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "yard-plot"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "yard-plot" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "yard-plot"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "yard-plot" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.
# input.json: the run input object - {"task": "review", "title": "...", "context": "...", "yard": "{...}", "items": "[...]", "checks": "{...}", "catalog": "[...]"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "review", "title": "...", "context": "...", "yard": "{...}", "items": "[...]", "checks": "{...}", "catalog": "[...]"}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task, title, context, yard, items, checks, catalog}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses yard-plot:<task>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="yard-plot:review:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "yard-plot:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("headline"))
import { createHash } from "node:crypto";
const key = `yard-plot:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.headline);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "yard-plot:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "yard-plot:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "yard-plot:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["headline"]].inspect
$key = "yard-plot:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["headline"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"yard-plot:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: yard-plot:review:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "review", "title": "...", "context": "...", "yard": "{...}", "items": "[...]", "checks": "{...}", "catalog": "[...]"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "yard-plot:review:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `yard-plot:review:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "yard-plot:review:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "yard-plot:review:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "yard-plot:review:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: yard-plot:review:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"yard-plot:review:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);