Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,26 @@ Leave `RESEND_API_KEY` unset and links are printed to the server log instead, wh
is what you want on a laptop. They are never shown in the browser: whoever typed an
address is not necessarily whoever owns it.

### Writing a listing with a model

An employer with a brief and no time can have a model expand it into the form. Set
one key and the box appears on `/post`; set neither and it does not:

```bash
OPENAI_API_KEY=sk-... # or ANTHROPIC_API_KEY=sk-ant-...
WRITER_MODEL=gpt-5.2-codex # optional; defaults to gpt-5.2 or claude-opus-5
```

It fills the form in and stops. Nothing is written and nothing is published: the
person who asked reads and edits every field, and it still becomes a draft after
that. This is the same seam an employer's agent goes through over the API, which is
the whole point of the board.

It will not invent compensation. If your brief says nothing about pay, every salary
field comes back empty, because a number nobody agreed to is worse than no number.
Drafting needs an account and is capped at ten an hour per account, so a board with a
key configured is not a public text generator.

## Boards find each other

Instances are independent. Each one has its own database, its own domain and its own
Expand Down
19 changes: 19 additions & 0 deletions migrations/0013_agent_drafts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- One row per time somebody asked a model to draft a listing.
--
-- This exists to be counted, not to be read. A board with a key configured is
-- holding an account somebody pays for, and a form that calls a model is a
-- proxy to it: without a ceiling, one script turns the employer's key into a
-- public text generator. Signed in is already required; this is the second
-- half of that.
--
-- Rows are kept rather than deleted after the window so that abuse is
-- visible after the fact. Nothing reads the brief back, so the brief is not
-- stored: it is the employer's unpublished writing and the count is the only
-- part this board needs.
create table if not exists agent_drafts (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references users(id) on delete cascade,
created_at timestamptz not null default now()
);

create index if not exists agent_drafts_user_recent on agent_drafts (user_id, created_at desc);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@profullstack/agenticjobs",
"version": "0.9.0",
"version": "0.10.0",
"description": "An agent-friendly job board you self-host. It posts its own jobs, never scrapes anyone else's, and answers on every surface: web, API, MCP, CLI, TUI, desktop and PWA. Instances find each other through an open directory.",
"license": "MIT",
"type": "module",
Expand Down
17 changes: 16 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ export interface Config {
/** Resend API key, or null to print sign-in links instead of sending them. */
resendApiKey: string | null;
mailFrom: string;
/**
* Model keys for agent-assisted drafting on the post form.
*
* Whichever is set turns the feature on; neither leaves it off and absent
* from the page rather than present and broken. Two providers because this
* is software other people self-host, and a board that only works if you
* bank with one vendor is not self-hostable.
*/
anthropicApiKey: string | null;
openaiApiKey: string | null;
/** Overrides the per-provider default model. */
writerModel: string | null;
version: string;
}

Expand Down Expand Up @@ -101,12 +113,15 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
isDirectory: flag(env['DIRECTORY']),
resendApiKey: env['RESEND_API_KEY']?.trim() || null,
mailFrom: env['MAIL_FROM']?.trim() || defaultMailFrom(publicUrl, boardName),
anthropicApiKey: env['ANTHROPIC_API_KEY']?.trim() || null,
openaiApiKey: env['OPENAI_API_KEY']?.trim() || null,
writerModel: env['WRITER_MODEL']?.trim() || null,
version: env['npm_package_version']?.trim() || VERSION,
};
}

/** Kept in step with package.json by the release script. */
export const VERSION = '0.9.0';
export const VERSION = '0.10.0';
export const SOFTWARE_NAME = 'agenticjobs';

/**
Expand Down
Loading
Loading