← Giving Jars
Giving Jars API
Tokens

Drive Giving Jars from your own code

The one metered run, plan, is available over HTTP. It takes a US household's gifts for the year (cause, amount, how each is paid) and the tax facts they chose to share, and returns a giving plan: a route for every gift - cash, appreciated shares, an IRA charitable distribution (qcd) or a donor-advised fund (daf) - the deduction path (standard / itemize / bunch), up to five dated moves before December 31, the paperwork to keep and questions for a tax professional, with a keep / tweak / restructure verdict. A natural use is a budgeting or donation-tracking app: your code sends the year's pledges in November and gets back a checklist with dates. Estimates, not tax advice.

The division of labour first: the page does the arithmetic; the model writes; the page re-checks the reply. Parsing the gift lines, the 2026 standard deduction (from irs.gov), the non-itemizer cash-gift deduction ($1,000, $2,000 joint, from irs.gov), the 0.5%-of-income floor on itemized gifts, IRA-gift eligibility, the alternatives with their estimated savings and the page's own lean all come from jarkit.js; the reply checks come from jarreply.js. Both run in node as well as in the browser. check(reply, state) holds a plan to the numbers: every gift placed exactly once, an IRA distribution only when one is open and never to a donor-advised fund, a deduction path the arithmetic supports, savings within reach of the page's estimates, moves dated on or before December 31, and no internal labels in prose. The age-65 add-on, the 0.5% floor, the IRA gift limit, the capital-gains rate and the custodian dates are estimates.

Get a token on the token page (step 2 below also shows how to mint a guest token in code).

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":"giving-jars"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or task is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the plan is metered and needs a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field

Every run sends task: "plan" first (the only task). The declared input schema carries scalars only, so the two object-valued fields travel JSON-encoded as strings (JarKit.toWire does this). The schema requires task.

Input fields

FieldTypeNotes
task, today, tax_yearstringplan; YYYY-MM-DD; 2026.
filing, ages, bracketstringsingle / married filing jointly / married filing separately / head of household; one or two ages like 52, 50 or not given; the top federal rate like 24%.
agi, other_itemized, rmdstringWhole dollars or not given: income estimate, deductions other than gifts that could be itemized, the year's required minimum distribution.
ira, holds_appreciated, next_yearstringyes / no; yes / no; about the same / more / less / not sure.
giftsstring (JSON)[{id, name, amount, pay_with, basis, monthly, flagged_not_deductible}] with ids g1, g2 … at most 12; pay_with is cash / stock / qcd / daf; monthly: true means twelve monthly gifts already added up.
page_factsstring (JSON){standard_deduction, itemized_total, charity_floor, deduction_now, non_itemizer_cap, non_itemizer_used, qcd_eligible, qcd_limit_estimate, tax_value_now_estimate, opportunities:[{kind, est_saving, gifts, detail}], page_lean, warnings, deadlines} - kind is qcd_swap, bunch, daf_to_cash or shares.
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object: verdict (keep - the plan is already the best path the page found; tweak - same gifts, change how some are paid; restructure - bunch two years into this one), verdict_reason, summary, deduction (standard / itemize / bunch), jars [{id, pay_with, when, why}] (one per gift), moves [{title, detail, by, est_saving}] (1-5), paperwork and ask_a_pro (strings). Parse with JarKit.parseReply, then normalize, then check(reply, state).

Worked examples

Replies produced against the app's prompt for the sample households (made up; lists shortened; the full replies ship in example-result.js). Together they cover every verdict. The page replays each of them free from its sample buttons.

plan - three small cash gifts under the non-itemizer cap - keep

Input:

{
 "task": "plan",
 "today": "2026-10-20",
 "tax_year": "2026",
 "filing": "married filing jointly",
 "ages": "44, 42",
 "agi": "140000",
 "other_itemized": "9000",
 "bracket": "22%",
 "ira": "no",
 "rmd": "not given",
 "next_year": "about the same",
 "holds_appreciated": "no",
 "gifts": "[{\"id\":\"g1\",\"name\":\"Riverside Food Bank\",\"amount\":600,\"pay_with\":\"cash\",\"basis\":null,\"monthly\":false,\"flagged_not_deductible\":false},{\"id\":\"g2\",\"name\":\"Kids reading program\",\"amount\":500,\"pay_with\":\"cash\",\"basis\":null,\"monthly\":false,\"flagged_not_deductible\":false},\"... 1 more\"]",
 "page_facts": "{\"standard_deduction\":32200,\"itemized_total\":9800,\"deduction_now\":\"standard\",\"qcd_eligible\":\"no\",\"page_lean\":\"keep\",\"opportunities\":[],\"...\":\"warnings, deadlines\"}"
}

Reply:

{
 "task": "plan",
 "verdict": "keep",
 "verdict_reason": "Your cash gifts already use the standard deduction along with the new cash-gift deduction for non-itemizers, and the page found no switch worth $50 or more.",
 "summary": "You're giving $1,500 in cash to three charities this year, taking the standard deduction along with the new deduction for cash gifts that applies even without itemizing. That combination is worth about $330 in tax value. The only thing left to do is make each gift by December 31.",
 "deduction": "standard",
 "jars": [
  {
   "id": "g1",
   "pay_with": "cash",
   "when": "by Dec 31",
   "why": "Paying by check, card, or cash before year-end keeps this gift in 2026 and lets it count toward the cash-gift deduction for non-itemizers."
  },
  {
   "id": "g2",
   "pay_with": "cash",
   "when": "by Dec 31",
   "why": "Keeping this as a cash gift made before year-end works the same way, adding to the total that counts toward that deduction."
  },
  "... 1 more"
 ],
 "moves": [
  {
   "title": "Make all three gifts by December 31",
   "detail": "Send your gift to Riverside Food Bank, the kids' reading program, and the animal shelter by check, card, or cash before the year ends so each one counts for 2026.",
   "by": "2026-12-31",
   "est_saving": null
  },
  {
   "title": "Keep the plan as it stands",
   "detail": "You're already using the standard deduction plus the new cash-gift deduction for non-itemizers, so there's no extra step needed for a bigger benefit this year.",
   "by": "",
   "est_saving": null
  }
 ],
 "paperwork": [
  "Get a written acknowledgment letter from each charity, since every gift here is $250 or more.",
  "... 1 more"
 ],
 "ask_a_pro": [
  "Would bunching two years of giving into one ever make sense for us if our other deductible expenses grow?",
  "... 1 more"
 ]
}

plan - a retiree, 74, with an IRA - tweak

Input:

{
 "task": "plan",
 "today": "2026-10-20",
 "tax_year": "2026",
 "filing": "single",
 "ages": "74",
 "agi": "62000",
 "other_itemized": "6000",
 "bracket": "12%",
 "ira": "yes",
 "rmd": "14000",
 "next_year": "not sure",
 "holds_appreciated": "no",
 "gifts": "[{\"id\":\"g1\",\"name\":\"St. Brendan parish year-end pledge\",\"amount\":4800,\"pay_with\":\"cash\",\"basis\":null,\"monthly\":false,\"flagged_not_deductible\":false},{\"id\":\"g2\",\"name\":\"County food pantry\",\"amount\":1000,\"pay_with\":\"cash\",\"basis\":null,\"monthly\":false,\"flagged_not_deductible\":false},\"... 2 more\"]",
 "page_facts": "{\"standard_deduction\":18150,\"itemized_total\":11990,\"deduction_now\":\"standard\",\"qcd_eligible\":\"yes\",\"page_lean\":\"tweak\",\"opportunities\":[{\"kind\":\"qcd_swap\",\"est_saving\":636}],\"...\":\"warnings, deadlines\"}"
}

Reply:

{
 "task": "plan",
 "verdict": "tweak",
 "verdict_reason": "Sending the parish, the food pantry, and the public radio station gifts straight from your IRA instead of paying cash lets each one count toward your required withdrawal and is worth more than paying by check, while the rest of your plan stays the same.",
 "summary": "You're likely better off taking the standard deduction this year rather than itemizing, so the real win here is in how you pay, not what you claim. Ask your IRA custodian to send the parish, the food pantry, and the public radio station gifts (about $6,300 total) straight from your IRA instead of by cash or check - that counts toward your required withdrawal and is worth about $636 in estimated savings. The gift toward your neighbor's medical costs isn't a charitable gift under the tax rules, so it simply stays a cash gift with no deduction.",
 "deduction": "standard",
 "jars": [
  {
   "id": "g1",
   "pay_with": "qcd",
   "when": "start by Dec 1",
   "why": "Giving this straight from your IRA instead of by cash lets it count toward your required withdrawal and keeps that amount out of your taxable income."
  },
  {
   "id": "g2",
   "pay_with": "qcd",
   "when": "start by Dec 1",
   "why": "Routing this one from your IRA too adds to the amount counted toward your required withdrawal, for a bit more estimated savings."
  },
  "... 2 more"
 ],
 "moves": [
  {
   "title": "Ask your IRA custodian to send three gifts directly",
   "detail": "Request a charity-direct distribution from your IRA for the parish, the food pantry, and the public radio station (about $6,300 total) instead of paying by cash or check. It counts toward your required withdrawal and is worth about $636 in estimated savings.",
   "by": "2026-12-01",
   "est_saving": 636
  },
  {
   "title": "Make sure every gift is sent before year-end",
   "detail": "Whether paid by check, card, or straight from the IRA, each gift needs to be out the door by December 31 to count for this tax year.",
   "by": "2026-12-31",
   "est_saving": null
  }
 ],
 "paperwork": [
  "Keep the IRA custodian's written confirmation that each of the three gifts went straight to the charity.",
  "... 1 more"
 ],
 "ask_a_pro": [
  "How much do you expect to give next year, in case it would ever be worth grouping two years of gifts together?",
  "... 2 more"
 ]
}

plan - a couple just under the standard deduction - restructure

Input:

{
 "task": "plan",
 "today": "2026-10-20",
 "tax_year": "2026",
 "filing": "married filing jointly",
 "ages": "52, 50",
 "agi": "260000",
 "other_itemized": "22000",
 "bracket": "24%",
 "ira": "yes",
 "rmd": "not given",
 "next_year": "about the same",
 "holds_appreciated": "yes",
 "gifts": "[{\"id\":\"g1\",\"name\":\"Alma mater scholarship fund\",\"amount\":6000,\"pay_with\":\"cash\",\"basis\":null,\"monthly\":false,\"flagged_not_deductible\":false},{\"id\":\"g2\",\"name\":\"Children's hospital\",\"amount\":3000,\"pay_with\":\"cash\",\"basis\":null,\"monthly\":false,\"flagged_not_deductible\":false},\"... 1 more\"]",
 "page_facts": "{\"standard_deduction\":32200,\"itemized_total\":31200,\"deduction_now\":\"standard\",\"qcd_eligible\":\"no\",\"page_lean\":\"restructure\",\"opportunities\":[{\"kind\":\"bunch\",\"est_saving\":1320},{\"kind\":\"shares\",\"est_saving\":0}],\"...\":\"warnings, deadlines\"}"
}

Reply:

{
 "task": "plan",
 "verdict": "restructure",
 "verdict_reason": "Putting this year's and next year's giving into one donor-advised fund gift lets you itemize this year and take the standard deduction next year, which the page values at about $1,320 over the two years - more than any other option it found.",
 "summary": "Giving cash straight to each group this year barely helps on taxes, worth only about $480, because your deductible expenses fall just short of itemizing. The bigger move is to put this year's and next year's giving into a donor-advised fund now - that pushes you to about $41,700 in itemized deductions this year, then you'd take the standard deduction next year, together worth roughly $1,320. You might also fund part of it with long-held shares that have grown in value instead of cash, which would skip capital-gains tax on the growth, though the page couldn't put a number on that without knowing your cost basis.",
 "deduction": "bunch",
 "jars": [
  {
   "id": "g1",
   "pay_with": "daf",
   "when": "by Dec 15",
   "why": "Folding this gift, along with next year's, into one donor-advised fund gift this year is the move the page estimates is worth about $1,320 over two years, instead of giving cash straight to the scholarship fund."
  },
  {
   "id": "g2",
   "pay_with": "daf",
   "when": "by Dec 15",
   "why": "Grouping this gift into the same donor-advised fund as the other two helps you clear the amount needed to itemize this year instead of giving cash directly."
  },
  "... 1 more"
 ],
 "moves": [
  {
   "title": "Fund a donor-advised fund with this year's and next year's giving",
   "detail": "Put all three of this year's gifts (about $10,500 total) plus about $10,500 more for next year into the fund now, so you can itemize about $41,700 this year and take the standard deduction next year - about $1,320 more between the two years than giving cash year by year.",
   "by": "2026-12-15",
   "est_saving": 1320
  },
  {
   "title": "Consider funding part of it with shares that have grown in value",
   "detail": "If you hold shares you've owned more than a year that have gained value, giving those instead of cash for the larger gifts would skip the capital-gains tax on the growth. The page couldn't estimate a dollar value for this without knowing what you paid for the shares.",
   "by": "2026-12-01",
   "est_saving": 0
  },
  "... 1 more"
 ],
 "paperwork": [
  "Keep the sponsoring organization's receipt for what you put into the donor-advised fund - that's your record for the deduction.",
  "... 1 more"
 ],
 "ask_a_pro": [
  "If we give shares that have grown in value, what's our cost basis and how long have we held them, so we know the capital-gains tax we'd actually skip?",
  "... 1 more"
 ]
}

Build the input yourself

Load both scripts in node (global.window = {}; require("./jarkit.js"); require("./jarreply.js")), then:

const K = JarKit;
const st = K.state({
  filing: "single", ages: "74", agi: "62000", other: "6000", rate: "12", ira: "yes", rmd: "14000", next: "unsure", appreciated: "no",
  gifts: "Parish 4,800 check\nFood pantry 1000 check\nPublic radio 500 card"
});
console.log(st.now.std, st.now.itemize, st.qcd, st.ops, st.lean);
const wire = K.toWire(K.buildPlan(st, { today: "2026-10-20" }).input);   // POST to /estimate and /run
// later: const plan = K.normalize(K.parseReply(job.output.output)); K.check(plan, st).notes

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://giving-jars.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"}
}

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":"giving-jars"}' | jq -r '.data.token')

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}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "plan", "today": "2026-10-20", "tax_year": "2026", "filing": "married filing jointly", "ages": "52, 50", "agi": "260000", "bracket": "24%", "gifts": "[{\"id\":\"g1\", ...}]", "page_facts": "{\"page_lean\":\"restructure\", ...}"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the task and the input: a retry with the same key never bills twice. The page uses giving-jars:<task>:<inputHash>:a<attempt> (task is always plan), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="giving-jars:plan:$(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}' 

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: giving-jars:plan:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json