Examples · 06 of 06
Maker-checker approval: one person asks, another approves.
Two-person approval for a payment typed in plain words. A reflex is a small program you install and ask for in your words. Here one reflex pays a payee on your list. The maker's sentence becomes a decision that waits as plain data until the checker, a second person, confirms it. Paying is destructive, so it never runs without a yes, and in an app your code decides who gives it.
The request and the result
01The maker types. The checker confirms.
The decision travels from one person to the other through a queue. Deciding and paying are two separate moments. Recorded on 21 September 2026 against Jev, TypeSafe AI's model, which reads each sentence.
- 1The maker's sentence becomes a call. The payee and the account are words from your lists. The amount is a number read from the sentence, between 1 and 250,000.
- 2The decision waits as plain data. It holds the call, the effect, the least sure answer and why it stopped. «weakest: route 1.00» means the least sure answer was the choice of reflex, at 1.00.
- 3Only
confirmed: trueruns it. The checker reads the call and the reason, then runs that same decision. In this example, one file turns the decision into JSON and back. Your app would store it between the two people.
The operations
02One payment reflex, used by the maker and the checker.
The payment reflex as a file and as code, your payees, and the checker that runs a queued decision.
In code, with() gives the same reflex each team's own payees and accounts.
reflex = 1 description = """ Pay an approved payee from one of our accounts. One transfer, released after a second person confirms it.""" not_for = [ "adding or changing a payee", "moving money between our own accounts", "asking a balance", ] tags = ["treasury"] effect = "destructive" confirm = "Pay {payee} {amount} from {account}?" run = "pay.mts" [needs] hosts = ["*"] # the payments API [config] api = "The payments API address" token = { about = "The payments API token", secret = true } [args.payee] ask = "Which payee?" vocab = "payees" [args.account] ask = "From which account?" vocab = "accounts" [args.amount] ask = "How much?" pick = "number" range = [1, 250000] [args.ref] ask = "What reference?" pick = "quoted" optional = true [examples] 'pay Acme 12400 from ops, ref "8812"' = { amount = "12400", ref = "8812" } "send 950 to the cleaners from the office account" = { amount = "950" } "wire 3200 to Acme from treasury" = { amount = "3200" } [tests] 'settle 18000 with Acme, ref "Q3"' = { amount = "18000", ref = "Q3" } "pay Acme from ops" = { amount = false, ref = false } "add Acme as a new payee" = false
acme = { what = "Acme, the packaging supplier.", value = "py_acme" } cleaners = { what = "Blitz, the office cleaners.", value = "py_blitz" }
import { load, reflex } from "@evoke-build/evoke" import { jev } from "@evoke-build/evoke/jev" const pay = reflex({ description: "Pay an approved payee from one of our accounts.\n" + "One transfer, released after a second person confirms it.", not_for: [ "adding or changing a payee", "moving money between our own accounts", "asking a balance", ], effect: "destructive", confirm: "Pay {payee} {amount} from {account}?", args: { payee: { ask: "Which payee?", vocab: "payees" }, account: { ask: "From which account?", vocab: "accounts" }, amount: { ask: "How much?", pick: "number", range: [1, 250000] }, ref: { ask: "What reference?", pick: "quoted", optional: true }, }, examples: { 'pay Acme 12400 from ops, ref "8812"': { amount: "12400", ref: "8812" }, "send 950 to the cleaners from the office account": { amount: "950" }, }, }, async ({ payee, account, amount, ref }, { signal }) => { const response = await fetch(`${process.env.PAYMENTS_API}/transfers`, { method: "POST", headers: { authorization: `Bearer ${process.env.PAYMENTS_TOKEN}`, "content-type": "application/json", }, body: JSON.stringify({ from: account, to: payee, amount, reference: ref }), signal, }) if (!response.ok) throw new Error(`payments answered ${response.status}`) const { id } = (await response.json()) as { id: string } return { text: `paid ${amount} from ${account}, transfer ${id}`, data: { id } } }) // This desk's payees, from the database; nothing else is a word. const desk = (await load({ reflexes: { pay }, adapter: jev() })).with({ vocab: { payees: { acme: { what: "Acme, the packaging supplier.", value: "py_acme" }, cleaners: { what: "Blitz, the office cleaners.", value: "py_blitz" }, }, accounts: { ops: { what: "The operations account.", value: "acc_ops" }, office: { what: "The office account.", value: "acc_office" }, }, }, }) // maker const decided = await desk.decide('pay Acme 12400 from ops, ref "invoice 8812"') if (decided.outcome !== "confirm") throw new Error(decided.outcome) // through the queue and back: a decision is plain data const queued: typeof decided = JSON.parse(JSON.stringify(decided)) // checker: the call, then the effect, the weakest judgment and why it stopped console.log(queued.call) console.log(queued.prompt.own) console.log((await desk.run(queued, { confirmed: true })).text)
// The desk decides; the decision waits in a queue as plain data; // a second person reads the call and runs it. import { load } from "@evoke-build/evoke" const desk = await load({ root: import.meta.dirname }) // maker const decided = await desk.decide('pay Acme 12400 from ops, ref "invoice 8812"') if (decided.outcome !== "confirm") throw new Error(decided.outcome) // through the queue and back: a decision is plain data const queued: typeof decided = JSON.parse(JSON.stringify(decided)) // checker: the call, then the effect, the weakest judgment and why it stopped console.log(queued.call) console.log(queued.prompt.own) console.log((await desk.run(queued, { confirmed: true })).text)
- 1A payee is a word. The classifier reads the word and its meaning. The program gets its value, the payee's id in your payments system. No sentence can name a payee you did not list.
- 2The token is a secret. The manifest declares it, and you name the variable that holds it. The value is read from the environment for one run, and never stored.
- 3The decision survives a trip through JSON.
run()takes it back, and refuses one made under other reflexes or other words. So the checker loads the same ones as the maker.
One boundary
03It asks for the missing amount, then asks for a yes.
At a terminal, the same reflex asks for what the sentence lacked, then asks for a yes. A no exits with 2, and nothing was paid.
$ evoke "pay Acme from ops" How much? > 12400 pay payee="acme" account="ops" amount="12400" · destructive · weakest: route 1.00 Pay acme 12400 from ops? [y]es [n]o [t]each > n [2]
evoke was sure at 1.00, and it still asked. At a terminal, no flag skips a destructive reflex's
question. In an app, only run(d, { confirmed: true }) runs it, so your code gives the yes and
decides who may. Every value is one the reflex allows, but a reading can still be wrong, so a payment always
waits for a yes. What the classifier cannot do
→ Why a payment always waits for a yes →
Set it up
04Pass one payment from maker to checker, with the SDK.
code.ts above holds the reflex, your lists, the maker and the checker, so it runs on
its own. Point it at a test instance of your payments API: the checker's run makes a real transfer.
- Node 24.5 or newer, and the SDKFor the maker and the checker in your app.
- A payments APIIts address in
PAYMENTS_API, and its token inPAYMENTS_TOKEN. The program posts one transfer. - A classifier keyGet a key from TypeSafe AI. Each payment typed is one request, billed to your key.
- Authorization, and a queue you trustevoke makes the decision. Who may make one, who may confirm it, and where it waits are your application's. The checker approves what the stored decision holds, so keep it where only your app can write.
-
Make the project
A folder for the app, with a package of ES modules and the SDK.
mkdir -p ~/approval && cd ~/approval && npm init -y && npm pkg set type=modulenpm install @evoke-build/evoke -
Save the file, with your lists
Save
code.tsfrom the code tab above in~/approval. Put your payees and accounts in its lists, with the ids your payments system uses as values. -
Pass one payment through
The maker's sentence is decided and sent through JSON. The checker prints the call and why it stopped, then gives the yes itself, with
confirmed: true, so the transfer is made at once. Your app would show the call to a second person, and passconfirmed: trueonly on their yes.export TYPESAFE_API_KEY=<value>export PAYMENTS_API=<address>export PAYMENTS_TOKEN=<value>node code.ts
In an app that serves several teams, pass each team's lists with with(). The checker
must build the same lists, or run() refuses the decision.