Skip to content

Instantly share code, notes, and snippets.

@zapnap
Last active September 4, 2026 19:01
Show Gist options
  • Select an option

  • Save zapnap/c570b0e62e50f767ad56ce4d42aa41c1 to your computer and use it in GitHub Desktop.

Select an option

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)
// 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