[bot] name = "cartero" interval = "10m" run_once = false [storage] # Postgres with the pgvector extension is required by the existing migrations. type = "postgres" dsn = "postgres://cartero:cartero@localhost:5432/cartero?sslmode=disable" # Small embedding model used only for semantic dedup (embed_text + embed_dedupe). # type = "ollama" uses OLLAMA_HOST; type = "openai" calls any OpenAI-compatible # /embeddings endpoint at base_url. [platforms.embedder] type = "ollama" enabled = true [platforms.embedder.settings] embedding_model = "qwen3-embedding:0.6b" # Jev evaluates every article against [interests]; required when interests are set. [platforms.jev] type = "jev" enabled = true [platforms.jev.settings] api_key = "${TYPESAFE_API_KEY}" # Pin a version so thresholds keep meaning the same thing; jev-latest can move. model = "jev-1.13.0" [platforms.discord] type = "discord" sleep = "1s" [platforms.discord.settings] bot_token = "YOUR_BOT_TOKEN_HERE" timeout = "60s" [platforms.bluesky] type = "bluesky" enabled = false [platforms.bluesky.settings] identifier = "your-handle.bsky.social" password = "your-app-password" [platforms.telegram] type = "telegram" enabled = false [platforms.telegram.settings] tg_bot_token = "YOUR_TELEGRAM_BOT_TOKEN_HERE" [sources.hackernews] type = "hackernews" enabled = true targets = ["discord_hn"] [sources.hackernews.settings] story_type = "topstories" max_items = 30 [sources.lobsters] type = "lobsters" enabled = false targets = ["discord_lobsters"] [sources.lobsters.settings] max_items = 20 sort_by = "hot" include_categories = ["rust", "go", "programming"] exclude_categories = ["rant", "culture"] [sources.rss_example] type = "rss" enabled = false targets = ["discord_rss"] [sources.rss_example.settings] feed_url = "https://example.com/feed.xml" max_items = 20 [sources.lesswrong] type = "lesswrong" enabled = false targets = ["discord_lw"] [sources.lesswrong.settings] max_items = 20 [sources.scraper_internal] type = "scraper" enabled = false targets = ["feed_target"] [sources.scraper_internal.settings] scraper_type = "internal" scraper_name = "techcrunch" max_items = 20 [sources.scraper_internal.settings.config] [sources.scraper_external] type = "scraper" enabled = false targets = ["feed_target"] [sources.scraper_external.settings] scraper_type = "external" script_path = "./scripts/custom_scraper.lua" max_items = 20 [sources.scraper_external.settings.config] custom_param = "value" [processors.dedupe] type = "dedupe" enabled = true [processors.dedupe.settings] ttl = "24h" # Semantic dedup: embeds titles and drops articles whose title is at least # embed_threshold similar to one published within embed_window, before Jev. [processors.embed_text] type = "embed_text" enabled = true [processors.embed_dedupe] type = "embed_dedupe" enabled = true [processors.embed_dedupe.settings] embed_threshold = 0.8 embed_window = "168h" [processors.score_filter] type = "filter_score" enabled = true order = 2 [processors.score_filter.settings] min_score = 100 [processors.extract_text] type = "extract_text" enabled = true [processors.extract_text.settings] min_content_length = 100 concurrency = 12 timeout_seconds = 8 flaresolverr_url = "http://flaresolverr:8191" flaresolverr_timeout_ms = 60000 [interests] keywords_file = "https://gist.githubusercontent.com/you/id/raw/keywords.json" # Jev assigns each article a probability of matching none of the interests. # Articles at or above this are rejected and never re-evaluated. The article is # tagged with its most likely interest. off_topic_threshold = 0.4 # Jev rates each article's substance from 0 (worthless) to 4 (exceptional) using # the article text. Articles below this score are rejected as low_quality. min_quality = 2.0 # Lower bar for articles whose text could not be fetched, since Jev then only # sees the title and summary. min_quality_incomplete = 1.4 # Optional higher quality floors for specific sources. The key is the pipeline # source name. Each feed loaded from an OPML file is named "_", # so an arXiv feed under tech_blogs is tech_blogs_arxiv. The floor replaces # min_quality for that source, including when the article text is incomplete. [interests.source_min_quality] # tech_blogs_arxiv = 3.0 # Questions Jev answers for every article: the topic, a 0-4 quality score from # the levels below (lowest first), and yes/no flags that reject at threshold. [prompts.topic] instructions = "Which topic is this article about? Judge by its main subject, not passing mentions." none = "The article is unrelated to all of the other topics, such as general news, politics, lifestyle or culture." [prompts.quality] instructions = """ How valuable is this to an experienced software engineer who reads for depth rather than news? \ Judge the substance, not how popular its topic is. \ For a software project or tool, judge how useful and technically interesting the project is, not how long its description is. \ If content_status is incomplete, rate what the title and summary suggest instead of treating the article as empty.\ """ incomplete = "incomplete: the article text could not be fully retrieved" levels = [ "Worthless: spam, marketing or a sales pitch, SEO filler, AI-generated fluff, or no real content.", "Thin: a news blurb, rebrand or product announcement, link roundup, listicle, or opinion with little evidence or technical detail.", "Decent: a clear article with some useful information or perspective, or a working open-source project or tool with a clear description of what it does.", "Substantive: original insight, real technical depth, data, or hands-on experience an experienced engineer would learn from, or a notable project with real technical substance.", "Exceptional: an in-depth investigation, definitive deep-dive, novel result, or standout project worth saving and sharing.", ] [[prompts.flags]] name = "clickbait" question = "Is the title clickbait: does it tease, exaggerate or withhold what the article is about to bait clicks (e.g. 'You won't believe', 'This one trick', vague teasers) instead of plainly stating the subject?" threshold = 0.7 [[prompts.flags]] name = "advertisement" question = "Is this a sales pitch: content whose main purpose is getting readers to buy, subscribe to or sign up for a commercial product or service (landing page, sponsored post, webinar or event promo, customer case study)? Announcements of new AI models, software or library releases, and open-source projects are news, not sales pitches." threshold = 0.7 [[prompts.flags]] name = "research_slop" question = "Is this an unrefereed preprint (for example on arXiv) whose main contribution is a benchmark, evaluation, survey or a minor variation of existing work, rather than a new result or method worth reading?" threshold = 0.6 [blocklist] domains_file = "https://gist.githubusercontent.com/you/id/raw/blocklist.txt" [processors.rate_limiter] type = "rate_limit" enabled = false order = 4 [processors.rate_limiter.settings] limit = 10 window = "1m" [targets.discord_hn] type = "discord" enabled = true platform = "discord" [targets.discord_hn.settings] channel_id = "YOUR_CHANNEL_ID_HERE" channel_type = "forum" [targets.discord_lobsters] type = "discord" enabled = true platform = "discord" [targets.discord_lobsters.settings] channel_id = "YOUR_CHANNEL_ID_HERE" channel_type = "forum" [targets.discord_rss] type = "discord" enabled = false platform = "discord" [targets.discord_rss.settings] channel_id = "YOUR_CHANNEL_ID_HERE" channel_type = "text" [targets.bluesky_example] type = "bluesky" enabled = false platform = "bluesky" [targets.bluesky_example.settings] languages = ["en"] [targets.telegram_hn] type = "telegram" enabled = false platform = "telegram" [targets.telegram_hn.settings] chat_id = -1001234567890 [targets.feed_target] type = "feed" enabled = true [targets.feed_target.settings] port = "8080" feed_size = 100 max_items = 50