Skip to content

Configuration reference

papio loads TOML from ~/.config/papio/config.toml (on Windows, %APPDATA%\papio\config.toml) unless the global --config <path> option selects another file. Configuration is layered over the built-in defaults; unknown TOML fields are rejected. papio init writes a validated user-only config file and papio doctor reports readiness.

The tables below list every decoded key in internal/config. Paths beginning with ~/ are expanded when papio loads them.

Top-level keys

Key Type Default Effect and constraints
access_mode string empty Required before acquisition. Allowed values are conservative, assisted, and delegated; a fresh guided papio init chooses conservative. Conservative records institutional OpenURL availability without opening a handoff; assisted and delegated can route eligible exhaustion to browser handoff.
email string empty Contact identity for polite API pools. Sent to third parties: as a query parameter to Unpaywall (required) and OpenAlex (required), to Crossref when set, and in the User-Agent of DOI-registration lookups. See Privacy. Doctor fails when enabled Unpaywall has no email; enabled OpenAlex also requires an email and API key.
data_dir path string ~/.local/share/papio (Windows: %LOCALAPPDATA%\papio) Private writable data directory for the database, artifacts, socket, and default browser-adoption directory.

[fetch]

Key Type Default Effect and constraints
max_bytes integer bytes 104857600 (100 MiB) Maximum artifact-download size. It must be at least 1048576 (1 MiB).
timeout_seconds integer seconds 120 Fetch deadline. It must be at least 5 seconds.
allow_http_loopback boolean false Development and test override that permits HTTP loopback. Doctor warns while it is enabled; production policy is HTTPS-only.

[captures]

Key Type Default Effect and constraints
enabled boolean true Stores sanitized diagnostic page captures received from the extension in the local data directory. Disable it to keep no diagnostic page HTML.
max_per_host integer 10 Maximum retained captures for each host. It must be between 1 and 1000; when the limit is exceeded, the oldest captures for that host are removed.
max_age_days integer days 14 Maximum capture age. It must be between 1 and 365; older captures are removed.

The [captures] section is strict-mode configuration: an older daemon rejects a config containing it. Deploy the binary that supports this section together with the configuration change.

[pdf]

Key Type Default Effect and constraints
ocr_enabled boolean true Enables the OCR fallback. If it is enabled, doctor requires both pdftoppm and tesseract; disabling it makes image-only papers require review.
min_text_chars integer 400 Minimum extracted-text threshold used by PDF validation before OCR fallback is relevant.
max_ocr_pages integer 4 Maximum pages processed by the OCR fallback.
title_match_threshold number 0.6 PDF title-match threshold. It must be greater than 0 and no greater than 1.

[browser]

Key Type Default Effect and constraints
extension_id string empty The Chrome extension ID allowed to use the native host. It must be 32 characters from a through p; an empty value disables the bridge.
extension_ids list of strings empty Additional Chrome-family extension IDs allowed to reach the native host alongside extension_id — e.g. an Edge Add-ons store copy or a second keyed build, which carry different IDs than the Chrome Web Store package. Each is 32 chars ap. The manifest's allowed_origins lists extension_id plus every entry here.
firefox_extension_id string empty The Firefox (Gecko) add-on ID allowed to use the native host — papio@orgmentem.com for the built extension. Accepts an email-style ID or a braced GUID; an empty value disables the Firefox bridge.
direct_routes_enabled boolean true Enables papio's compiled-in provider-direct HTTPS route candidates when the connected extension advertises support. Set false to disable these routes daemon-wide; institutional OpenURL routing is unchanged.
openurl_base_url string URL empty Legacy/default institutional OpenURL resolver base. It must use https://; an empty value prevents default-profile institutional routing. Existing query parameters are preserved when papio adds citation metadata. Prefer the institution's direct-link-enabled endpoint so a single electronic service bypasses the resolver menu.
shibboleth_entity_id string URL empty Default institution's Shibboleth IdP entityID (https://). When set, a provider login wall is auto-routed to this IdP (skipping the WAYF selector). Empty disables federated login-routing for the default profile.
proquest_account_id string digits empty Default institution's ProQuest account id (digits, max 64). When set, papio appends ?accountid=<id> to unlock the institution's ProQuest link-resolver without a manual sign-in. Empty disables the append. During papio init you may paste a ProQuest URL containing accountid= instead of the bare id.
libkey_mode string empty LibKey routing for the default institution: empty or off disables it; link routes DOI/PMID handoffs through the keyless libkey.io/libraries/<id>/<doi-or-pmid> institution link ahead of the bare OpenURL resolver, falling back to OpenURL for works without a DOI or PMID and whenever LibKey is unavailable. api is reserved and rejected (not implemented). Requires libkey_library_id when set to link.
libkey_library_id integer 0 The institution's numeric Third Iron library id — the number in its BrowZine/LibKey.io URL (…/libraries/<id>/…). Required (positive) when libkey_mode = "link"; setting it without link mode is rejected rather than left silently dead.
default_resolver string empty Named [browser.resolvers.<name>] profile used when a request omits resolver (e.g. papio acquire without --resolver). Empty preserves the historical default institution. Must name a configured profile — the implicit default profile (when openurl_base_url is set) or a [browser.resolvers.<name>] key; an unconfigured name is rejected at load.
download_adoption_root path string empty Root for browser-download adoption; adoption is confined to a job subdirectory beneath it. When empty the effective value is <your download folder>/papio — on macOS ~/Downloads/papio, on Linux/BSD the XDG XDG_DOWNLOAD_DIR folder (falling back to ~/Downloads), on Windows the Downloads known folder. It must be a papio directory inside the folder your browser downloads into: Chrome can only steer a download to a path relative to its own download directory, so any other root adopts nothing at all, silently. papio init creates the effective root, and papio doctor's adoption_root check fails and names the resolved path when it is unreachable.
action_expiry_seconds integer seconds 1800 Browser-handoff expiry and the initial age before an open human action is reminded. Later reminders double their per-action interval through 24 hours. It must not be negative.

papio init can derive openurl_base_url from a pasted library discovery URL (--institution-url) or from the resolver configured in Zotero.

The browser path uses the user's ordinary Chrome session. It is not configured with passwords, MFA, CAPTCHA tokens, or publisher credentials.

papio init collects extension_id and firefox_extension_id during setup (Firefox defaults to the built add-on's papio@orgmentem.com), or set them non-interactively with --extension-id / --firefox-extension-id, so the native messaging host installs on the first run.

[browser.resolvers]

Named resolver profiles are per-institution tables keyed by a lowercase alphanumeric name; default is reserved for the implicit top-level institution and is rejected at load if used as a profile key. Each carries its own OpenURL base and, optionally, the same shibboleth_entity_id, proquest_account_id, libkey_mode, and libkey_library_id fields as the default [browser] institution — so a multi-institution user routes each job's login to the right library instead of inheriting the default's identity:

[browser.resolvers.campus]
openurl_base_url = "https://library.example.edu/discovery/openurl?institution=EXAMPLE"
shibboleth_entity_id = "https://idp.example.edu/idp/shibboleth"  # optional
proquest_account_id = "12345"                                     # optional
libkey_mode = "link"                                              # optional
libkey_library_id = 1234                                          # required for link mode

A profile may also be written as a bare string — campus = "https://library.example.edu/discovery/openurl?institution=EXAMPLE" — which is shorthand for a table with only openurl_base_url set. This keeps older single-base configs valid; add the table form when a profile needs its own login identity.

Select one with papio acquire --resolver campus, papio acquire --batch works.json --resolver campus, or the corresponding MCP field. The selected name is snapshotted in the job policy, so re-opened actions cannot silently fall back to another institution.

Set browser.default_resolver to one of these names (or leave it empty) to choose which profile an omitted resolver uses; an explicit --resolver/MCP resolver value always takes precedence, and an unconfigured default_resolver value is rejected at load.

On a tracked Alma/Primo resolver page, the extension may follow the first same-origin resolveService link selected by the institution's Online Services order. This emulates resolver direct linking without accepting provider terms or initiating physical-item, scan, or interlibrary-loan requests. Script access remains constrained by extension/manifest.json host permissions; an unlisted custom resolver origin stays in assisted mode.

[browser.document_delivery]

Configures the default institution's document-delivery / interlibrary-loan route (ADR-0017). Omitting the table disables it — a job that exhausts every acquisition candidate falls back to the profile's plain OpenURL route, the same behavior as before this table existed. The identical table nests under [browser.resolvers.<name>.document_delivery] for a named institution profile, alongside that profile's own openurl_base_url, shibboleth_entity_id, proquest_account_id, libkey_mode, and libkey_library_id.

Key Type Default Effect and constraints
kind string none (required) Delivery adapter: openurl | libkey | illiad | custom. Required — papio never guesses which ILL system an institution runs from branding or a landing page. oclc and rapido are named as intended future providers but rejected: a kind whose adapter has not shipped must not parse, the same fail-closed rule sources apply to [sources.<name>].
base_url string URL empty Request form or API base. Used as the request-form URL for openurl/custom, and as the ILLiad Web Platform base for illiad. Must use https://.
patron_web_base_url string URL empty Patron-facing ILLiad web client base (the illiad.dll URL patrons use), permitted only for kind = "illiad". Must use https://. Never derived from base_url — shared-server sites and customised directories make derivation unreliable. When set, a fulfilled request's "View PDF" page (form 75) routes through the ordinary browser handoff for retrieval; when empty, fulfillment falls back to a recorded human action and the compiled profile reports fulfillment: none even if submission is auto-capable.
allowed_hosts list of strings empty Hosts a prefilled request form or API base may reach.
submit_policy string never never (default) | prefill_only | auto_if_unconditional. Narrows what the daemon-wide access_mode permits — it never widens it: under conservative the route is discovered and recorded only, never opened or submitted; under assisted the prefilled form opens but submission stays human. auto_if_unconditional is accepted only when kind = "illiad": openurl, libkey, and custom route to a form with no deterministic submission-and-reconciliation contract, so they compile permanently prefill_only.
request_classes list of strings empty Request classes this profile is declared for. v1 recognizes only digital_journal_article; any other value is rejected as not yet modelled.
legal_basis string unknown institution_policy | copyright_act_s49 | unknown. Configured, never inferred from a hostname. copyright_act_s49 (Australian document supply) compiles prefill_only permanently, by statute: the patron's declaration is an affirmative, per-request statutory act — "not previously supplied" — that no standing declaration can truthfully cover, and papio must never tick, script, or represent it. An AU-jurisdiction profile defaults patron_attestation to unknown until the institution confirms otherwise.
patron_attestation string unknown not_required | standing_completed | per_request | unknown. standing_completed counts only when the institution has confirmed a registration-time agreement covers API-created requests of this class — never inferred from an account's existence, a missing checkbox in one render, an API accepting a request, or the institution's country or hostname.
patron_fee_policy string unknown zero_standard | per_request | unknown. Only zero_standard can ever compile auto_capable — v1 auto-submission covers zero-patron-fee digital journal articles only; books, chapters, theses, physical loans, rush service, and any nonzero or provider-quoted fee stay prefill_only.
monthly_request_cap integer 0 (no cap) Bounds auto-submitted requests per calendar month; 0 means no declared cap. Values below zero are tolerated and behave as 0 (no cap).
status_poll_minutes integer minutes 0 (adapter default) Delivery status poll cadence; 0 uses the delivery service's own default. Values below zero are tolerated and behave as 0 (the default). Delivery polling draws on its own budget, never on ordinary resolver/HTTP retry counts, so a slow ILL turnaround cannot exhaust the acquisition waterfall's retry budget.
api_key string empty Institution-issued application credential, permitted only for kind = "illiad" — a key on a form-kind profile is rejected as dead config. Read only by the daemon's delivery service; never sent to, stored in, or observable from the extension or the browser wire. 0600 config only.
patron_ref string empty Configured, non-secret patron reference used to map requests to the institution's system. Personal identity data, not a secret: 0600 config only, redacted from events, diagnostics, and delivery provenance.

A compiled auto_capable verdict additionally requires one recorded live acceptance — a supervised submit-and-reconcile against the real deployment under the institution's authority — so a compiled adapter plus matching config is necessary but not sufficient. papio init prints the compiled gate class before saving (AUTO-CAPABLE with its evidence, or PREFILL ONLY with the specific blocker) and papio doctor verifies what is verifiable while keeping DECLARED configuration and PASS/OBSERVED facts strictly separate: neither command claims automatic submission for a profile that cannot actually reach it.

[browser.resolvers.campus.document_delivery]
kind = "illiad"
base_url = "https://ill.campus.example.edu/illiadwebplatform"
allowed_hosts = ["ill.campus.example.edu"]
submit_policy = "auto_if_unconditional"      # or "prefill_only" / "never"
request_classes = ["digital_journal_article"]
legal_basis = "institution_policy"           # or "copyright_act_s49"
patron_attestation = "standing_completed"    # or "not_required" / "per_request"
patron_fee_policy = "zero_standard"
monthly_request_cap = 25
status_poll_minutes = 60
api_key = "..."
patron_ref = "configured-non-secret-reference"

[zotio]

Key Type Default Effect and constraints
executable path or command string zotio zotio executable papio invokes at the Zotero boundary. Optional: an empty value disables the deep Zotero integration (auto-import, plan/apply, queue). When no generic library.sources authority is configured, ownership lookup then classifies every work as not-owned. Required only when auto_import = true.
timeout_seconds integer seconds 120 zotio command deadline. It must be between 5 and 600 seconds inclusive.
attachment_mode string stored zotio attachment mode. Allowed values are stored and linked-file.
auto_import boolean false Default acquisition policy for automatic zotio plan-and-apply after a job is ready. An acquire --auto-import request can opt in per job.
auto_enrich boolean true After the first applied auto-import, enables the conservative scoped zotio enrichment of missing DOI and abstract fields for the imported parent.
exception_tags boolean false Enables the reconciled exception-tag ledger: papio maintains papio:needs-action and papio:unavailable as Zotero automatic tags on provenance-confirmed personal-library items, reconciling job state with current attachment state (papio zotio tags reconcile runs one pass on demand). Requires executable and zotio ≥ 0.13.0. Lifecycle states are never tagged; a same-name manual tag is never retyped or removed. After a daemon reload, turning this off makes the next pass remove papio-owned tags.
unavailable_recheck_days integer days 14 How long an unavailable outcome parks an item before backfill re-checks it (open-access availability drifts upward). Must be between 1 and 365 inclusive.

papio invokes zotio but does not read or store Zotero credentials. Manual mutation remains preview-first: papio zotio plan returns immutable plans and papio zotio apply requires the exact confirmation SHA-256.

[hooks]

Key Type Default Effect and constraints
on_ready shell command string empty When set, runs once via the system shell (/bin/sh -c; cmd /C on Windows) each time a job reaches ready (validated artifact). Job metadata arrives as PAPIO_* environment variables. Fire-and-forget: a failing hook is recorded as a hook.on_ready job event but never fails or retries the job. Empty disables it. See the hooks guide.
timeout_seconds integer seconds 120 Deadline for one hook run. Validated (5..600) only when on_ready is set.

[[library.sources]]

Libraries papio consults to answer "do I already hold this paper?" for users who do not run Zotero. Repeat the table for each source (maximum 8 — every one is read on every search, batch, and discovery acquire-watch pass). Generic library.sources are ignored while zotio.executable is configured; otherwise they are the ownership authority for discovery acquire watches. Alert watches retain their historical zotio ownership path and do not consult generic sources.

Key Type Default Effect and constraints
name string Required, unique, and must have no leading or trailing whitespace. Identifies the source in papio doctor output and in warnings.
kind string Required. file is the only supported kind; anything else is rejected rather than ignored.
path string Required for kind = "file". The bibliographic export to read. ~ is expanded and the resulting path must be absolute.
format string empty bibtex, ris, csl-json, or nbib. Empty detects from the path and content.
claim string Required, no default: pdf_present (entries whose full text you hold, so a match may skip acquisition) or record_present (citations only — annotates papio search but never skips).

Matching is exact on identifiers represented by the source format. No format supports every identifier, and titles are never matched. ISBN is excluded, because an edited volume shares one ISBN with every chapter in it. papio never infers PDF presence from per-manager attachment fields (BibTeX file, papis files) — the source declares it via claim.

Format DOI arXiv PMID
BibTeX yes yes yes
CSL-JSON yes no yes
NBIB yes no yes
RIS yes no no

A source unreadable to papio is reported as unreadable, not as holding nothing: papio acquire --batch then refuses to create jobs rather than re-downloading the whole batch. Before the fifth consecutive failure, each cadence attempts another run; a successful run resets the failure count. The fifth consecutive failure disables the watch; there is no re-enable command. After fixing the source, you may force-run it once with papio watch run <id>, but scheduled execution resumes only if you recreate the watch. --include-owned is available only for papio acquire --batch, meaning "proceed despite ownership uncertainty". Because a bibliographic export cannot say which manifestation it holds, such a source never satisfies an explicit --desired-version published request. papio doctor performs a fresh one-shot probe of each source and reports that read's record count and outcome; it does not report daemon cached age, count-collapse detection, or retained failure state. See the filing guide.

[notify]

The notification router has two independent legs: local desktop interruptions for a human, and webhook delivery for automation. Desktop delivery is best-effort and currently available only through the macOS sender; an unsupported platform is reported by papio doctor and does not consume the desktop rate budget. Webhooks are not subject to quiet hours, focused-surface suppression, or the desktop rate ceiling.

Key Type Default Effect and constraints
enabled boolean true Enables best-effort local desktop notifications from the daemon. This affects only the desktop leg; it does not disable webhooks.
preset string milestones Base desktop/webhook routing policy. Accepted values are quiet, milestones, and verbose; per-category overrides below layer on top.
max_per_hour integer 6 Maximum desktop notification reservations in a rolling hour. 0 means unlimited. Must be in 0..10000. This ceiling never applies to webhooks.
quiet_hours string empty (disabled) Optional local civil-time interval in HH:MM-HH:MM form. The start and end must differ; spring-forward and fall-back transitions are handled as one local interval.
quiet_mode string hold Behavior for desktop notices during quiet_hours: hold keeps them pending until the window ends; drop records them as dropped. Accepted values are hold and drop.
digest_every_minutes integer minutes 240 Interval for pending-decision digests. 0 uses the four-hour default. Must be in 0..10080.
completion_quiet_minutes integer minutes 10 Quiet period before a batch completion checkpoint may be surfaced. Must be in 0..1440.
completion_max_hold_minutes integer minutes 120 Maximum hold for a batch completion checkpoint. Must be in 0..10080; when nonzero, it must be at least completion_quiet_minutes.
stall_after_minutes integer minutes 30 Minimum named episode age before a system-degraded/stall notice. Must be in 0..10080. A scheduled retry or delivery poll is not a stall.
categories table of tables empty Per-category overrides. Keys must be one of the seven names listed below; an unknown category is rejected.
webhook_url string URL empty When set, notification intents are delivered as JSON POSTs to this automation endpoint. It is independent of enabled and desktop policy; must be an absolute http(s) URL.
webhook_secret string empty Sent as Authorization: Bearer <secret> on webhook posts. Requires webhook_url.

The milestones preset is the default: it sends immediate coalesced standalone outcomes and newly opened decisions, a four-hour pending-decision digest, a bounded batch checkpoint/final summary, discovery as catch-up or digest material, capped integrity notices, and one system-degraded notice per episode. Work that is continuing or scheduled does not produce a desktop interruption. The preset treatment is:

Category quiet milestones (default) verbose
request_outcome Immediate failures; digest successes Immediate, coalesced Immediate
decision_opened Digest Immediate, coalesced over 5 minutes Immediate, coalesced over 60 seconds
decision_pending Off Digest, at most every 4 hours Immediate, once per pass
completion_batch Off One checkpoint when useful plus one meaningful final delta One checkpoint when useful plus one meaningful final delta
discovery_new Off Catch-up or desktop digest Immediate, coalesced
integrity_notice Immediate, capped Immediate, capped Immediate, capped
system_degraded Immediate, state-deduped Immediate, state-deduped Immediate, state-deduped

[notify.categories.<name>]

Use one table for each category whose routing should differ from the preset:

[notify.categories.decision_opened]
desktop = "immediate"   # off | digest | immediate
webhook = "immediate"   # off | digest | immediate
window_seconds = 300

The valid category names are request_outcome, decision_opened, decision_pending, completion_batch, discovery_new, integrity_notice, and system_degraded. desktop and webhook each accept off, digest, or immediate; an empty value inherits the preset. window_seconds defaults to the preset's window when 0 (or when omitted) and otherwise must be in 0..86400. Webhook mode is independent automation routing: it remains immediate by default and ignores desktop quiet hours, focus suppression, and rate limits. A category may override its webhook mode without changing its desktop mode.

This section is strict-mode configuration. Deploy the binary that understands these fields together with the configuration change; an older daemon rejects unknown [notify] fields.

[updates]

Key Type Default Effect and constraints
check boolean false Enables the daemon's once-daily background check for a newer papio or zotio release against GitHub's public release API. Sends no identifier, count, or telemetry beyond the anonymous request GitHub receives for any web hit. See Privacy. papio init's guided setup suggests enabling it (non-interactive --check-updates also defaults true); the config default when the key is absent (or on a config written before this key existed) is false. papio doctor reports the check as skipped while it is off.

[discovery]

Key Type Default Effect and constraints
sources string array empty (= ["openalex"]) Discovery backends for papio search and watches, in merge-preference order. Valid entries: openalex, semanticscholar (each at most once). Results merge with DOI-then-title deduplication; earlier backends win ties. Per-backend API keys live under [sources.<name>] (e.g. sources.semanticscholar.api_key, optional — Semantic Scholar works keyless at public rate limits).

[actions]

Key Type Default Effect and constraints
stale_after_seconds integer seconds 604800 (7 days) How long an open human action may wait before listings report it stale. 0 selects the default; a negative value is rejected. papio actions list marks stale rows (stale and age_seconds in --json), and nothing else happens: papio never cancels, expires, or sweeps a handoff on a timer.

This is deliberately not browser.action_expiry_seconds. That one (30 minutes by default) is a reminder cadence — how soon to nudge you again about an open action — so reusing it as a staleness threshold would report a handoff queued over lunch as abandoned. This one answers "has anyone given up on this?", which is measured in days.

[sources.<name>]

[sources] is a map of resolver policies. Its keys are whitelisted separately from papio's general strict-unknown-field rejection: the map itself decodes freely, and validate() rejects an unrecognized key at load with the list below. The valid names are arxiv, europepmc, unpaywall, openalex, core, crossref_tdm, crossref_metadata, retraction_watch, semanticscholar, and openaire. One further name, openalex_content, is a removed key: an earlier papio release wrote it into Default() and no adapter for it ever shipped, so a config carrying it loads normally and drops the key silently rather than breaking on upgrade — it is not a valid key to add yourself. For semanticscholar, enabled governs the acquisition resolver (open-access PDF lookup by exact DOI, arXiv id, or PMID); selection as a search backend is separate and lives in [discovery] (which reads this section's api_key). For openaire, candidates come from the OpenAIRE Graph (metadata licensed CC-BY, acknowledged here and in candidate provenance); the keyless public limit is 60 requests/hour — the default rate_per_sec honors it, and a personal-token api_key raises the ceiling. Each named section accepts these keys:

Key Type Default Effect and constraints
enabled boolean source-specific; see below Enables the resolver policy.
api_key string empty Credential or token for a source that requires one. Doctor requires it for enabled openalex, core, and crossref_tdm; enabled OpenAlex also needs email. For openaire this holds a personal access token, which OpenAIRE expires one hour after issuing it — usable for a manual check, not for unattended operation; use client_id/client_secret instead.
client_id string empty OpenAIRE registered-service client id. Read only by openaire today. Unlike a personal access token these credentials do not expire, so this is the only OpenAIRE credential that survives unattended operation. Must be set together with client_secret; half a pair is ignored.
client_secret string empty Secret paired with client_id. papio exchanges the pair for a short-lived access token at OpenAIRE's AAI endpoint and re-exchanges it before expiry; the secret is never logged or echoed in an error.
rate_per_sec number source-specific; see below Per-source request-rate budget.
burst integer source-specific; see below Per-source burst budget.
max_cost_usd number 0 Monthly budget for paid sources. 0 means unmetered.
daily_credit_fraction number 0 (OpenAlex default 0.5) Fraction of the provider's observed daily credit limit the daemon may spend before the daily fuse parks the identity. Must be in [0,1]. OpenAlex ships 0.5 in built-in defaults; other sources default to 0 until a fuse adopts them.
daily_credit_limit integer credits 0 Absolute operator override and hard maximum for the daily credit fuse; the fraction may never exceed this cap. 0 means no absolute ceiling (unmetered allowance from the observed limit × fraction). The egress debit still happens when metering is active.
sibling_title_search boolean false OpenAlex fuzzy sibling title search — a 10-credit search= query that looks for an open-access copy of a paywalled work under a different DOI. Off by default: measured against the operator's own history it cost ≥138 credits per accepted artifact even under the most generous possible attribution (all 317 accepted artifacts attributed to 3,150 sibling-hop searches). When off, the hop makes no request at all and reports that it made none. Set true under [sources.openalex] to turn it back on.
allow_keep_alives boolean false When false (default), metadata transports open one connection per request — no HTTP keep-alive reuse, the replay-hygiene posture. Set true only with evidence that a high-rate source needs reuse; it is an opt-out of the safe default and requires per-source policy justification. HTTP/1-only negotiation is not configurable.
base_url_for_dev string URL empty Test/development endpoint override. If set, it must start with http://127.0.0.1 or http://localhost; do not use it for a remote production endpoint.
### Built-in source defaults

| Source name | enabled | rate_per_sec | burst | | arxiv | true | 1 | 1 | | europepmc | true | 2 | 2 | | unpaywall | true | 1 | 1 | | openalex | false | 2 | 2 | | openaire | true | 0.016 | 1 | | core | false | 0.4 | 1 | | crossref_tdm | false | 1 | 1 | | crossref_metadata | true | 1 | 1 | | retraction_watch | true | 1 | 1 | | semanticscholar | true | 1 | 1 |

openaire's rate_per_sec looks oddly precise because it is pinned to a documented ceiling: OpenAIRE allows 60 requests per hour to unauthenticated callers, and 0.016 × 3600 = 57.6. Do not raise burst to make bursty demand go through — the hourly budget is burst + rate_per_sec × 3600, so at this rate a burst above about 2 would breach the published ceiling.

To get more OpenAIRE throughput, authenticate: 7,200 requests per hour is 120× the keyless allowance, and it is free. OpenAIRE issues two kinds of credential and only one of them can run a daemon:

  1. Fill in the personal information form — the newer registration flow gates both credential types behind it.
  2. Go to Registered Services, click + New Service, pick the Basic security level, and create it. Copy the Client ID and Client Secret it shows you.
  3. Put them in [sources.openaire] as client_id and client_secret.

papio exchanges that pair for an access token as needed and refreshes it before it expires. The alternative credential — a personal access token — is a single string you can paste into api_key, but OpenAIRE expires it one hour after issuing it, so it is only useful for a manual check. papio doctor warns when api_key is carrying one.

Note that OpenAIRE reports x-ratelimit-limit: 7199 in its responses even to unauthenticated requests, so that header is not a safe basis for choosing rate_per_sec. Use the documented ceiling for the tier you are actually in.

Setting client_id/client_secret also raises rate_per_sec to 1.9 and burst to 5 (6,845 requests/hour against the documented 7,200), because a credential that authenticated but changed nothing observable would be two knobs for one intent. An explicit rate_per_sec or burst in your config always wins. A personal access token in api_key does not raise pacing: it authenticates for an hour, and pacing to the authenticated ceiling on a credential that can vanish mid-hour would leave papio running at 120× an unauthenticated allowance.

Watch configuration

There is no [watch] section or watch-specific key in papio's TOML config. Watch query, year filters, OA filter, collection, cadence, and per-run cap are stored with each watch created by papio watch add or the corresponding MCP tool. Use papio watch list to inspect them and papio watch remove <id> to remove one.

Validation and file permissions

papio validates configuration when loading and saving it. It writes the config file with mode 0600 and its config directory with mode 0700; doctor reports a configuration permission failure when group or other read bits are present. Use papio doctor rather than weakening these permissions to diagnose a setup problem.