Last active
September 4, 2026 19:01
-
-
Save zapnap/c570b0e62e50f767ad56ce4d42aa41c1 to your computer and use it in GitHub Desktop.
freeq: a bounty lifecycle end to end, driven by two bots (act events)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // A freeq bounty, end to end, driven by two bots. | |
| // | |
| // A bounty is an `act` action: a signed, stateful message with rules the | |
| // server enforces. The poster opens it, workers bid, the poster awards one | |
| // bid, the worker submits, the poster asks for revisions or accepts. Every | |
| // step is a TAGMSG signed by its sender; the server checks who may take the | |
| // step and from which state (spec/act-transitions.json) before it files | |
| // and relays it, and a step from the wrong party or the wrong state is | |
| // refused and never appears in the room. | |
| // | |
| // Run: npx tsx bounty-lifecycle.ts | |
| // Needs: @freeq/bot-kit and @freeq/sdk (both in the freeq repo), a freeq | |
| // server, and a channel to post in. | |
| import { FreeqBot } from '@freeq/bot-kit'; | |
| import { actTags } from '@freeq/sdk'; | |
| const CHANNEL = '#act-demo'; | |
| const URL = 'wss://irc.zerosum.org/irc'; | |
| const OWNER = 'did:plc:...'; // the person the bots act for | |
| const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); | |
| type Ctx = { client: any; target: string; did: string }; | |
| // One helper for every step: build the act tags for this kind and verb, | |
| // sign them, send them with a one-line sentence for the humans in the room. | |
| // `task` is the bounty's id (the offer's own event id); the offer has none. | |
| function step(ctx: Ctx, verb: string, task: string | undefined, fields: Record<string, string>, line: string) { | |
| return ctx.client.sendAct(ctx.target, actTags('bounty', verb, task, ctx.did, fields), { taskId: task, humanText: line }); | |
| } | |
| async function bot(nick: string): Promise<Ctx> { | |
| const b = await FreeqBot.create({ | |
| name: nick, | |
| nick, | |
| ownerDid: OWNER, | |
| url: URL, | |
| channels: [CHANNEL], | |
| capabilities: ['freeq.at/act'], | |
| actorClass: 'agent', | |
| }); | |
| await b.start(); | |
| return { client: b.client, target: CHANNEL, did: b.identity.did }; | |
| } | |
| async function main() { | |
| const poster = await bot('demo-poster'); | |
| const worker = await bot('demo-worker'); | |
| await sleep(2000); | |
| // 1. The poster opens the bounty. Its event id becomes the bounty's id. | |
| // `price` is opaque to the server: stored, signed, never interpreted. | |
| const bounty = await step(poster, 'offer', undefined, | |
| { title: 'Write a 200-word summary of the act RFC', price: '25 USD' }, | |
| 'offered a bounty: Write a 200-word summary of the act RFC · 25 USD · open to bids'); | |
| await sleep(3000); | |
| // 2. A worker bids. Bids are additive: the bounty stays open, any number | |
| // of bids can arrive, from any server. | |
| const bid = await step(worker, 'bid', bounty, { bid: '20 USD' }, 'bid 20 USD on the bounty'); | |
| await sleep(3000); | |
| // 3. The poster awards one bid by its event id. The server never picks a | |
| // winner; it only checks that the named event is a bid on this bounty. | |
| // The bid's author becomes the assignee. | |
| await step(poster, 'award', bounty, { accepts: bid }, 'awarded the bounty to demo-worker (20 USD bid)'); | |
| await sleep(3000); | |
| // 4. Only the assignee may report progress or submit. | |
| await step(worker, 'progress', bounty, { note: 'drafting the summary' }, 'in progress: drafting the summary'); | |
| await sleep(3000); | |
| await step(worker, 'submit', bounty, {}, 'submitted the work for review'); | |
| await sleep(3000); | |
| // 5. Only the poster may send it back or accept it. Submitted work waits | |
| // in review; the poster cannot cancel once work is delivered. | |
| await step(poster, 'revise', bounty, { note: 'trim to 200 words' }, 'asked for revisions: trim to 200 words'); | |
| await sleep(3000); | |
| await step(worker, 'submit', bounty, {}, 'resubmitted at 200 words'); | |
| await sleep(3000); | |
| await step(poster, 'accept-work', bounty, {}, 'accepted the work; bounty closed'); | |
| // The whole signed history, every step plus the server's receipt for each | |
| // state change, is readable by anyone who can read the channel: | |
| // GET /api/v1/actions/<bounty id> | |
| console.log(`bounty ${bounty}: done`); | |
| } | |
| main().catch((e) => { console.error(e); process.exit(1); }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment