Packages
polymarket_notify
0.1.0
Notification controls (per-category x per-strategy) plus pluggable channels (Telegram/Discord/webhook/log) for Elixir trading bots. Best-effort delivery, injectable HTTP, no processes or database.
Current section
Files
Jump to
Current section
Files
polymarket_notify
README.md
README.md
# PolymarketNotify
Notification **controls + channels** for Elixir trading bots. A small,
dependency-light layer that decides *what* to notify about, *for which
strategy*, and *where* to send it — then fans a plain-text message out
to pluggable delivery channels (Telegram, Discord, generic webhook, or
the Logger).
It was extracted from a Polymarket prediction-market trading bot and
generalized: nothing here is Polymarket-specific. Strategies are
identified by an arbitrary string **tag**, so a user-written strategy
gets its own notification settings with no code change.
This package is part of the Polymarket Elixir ecosystem (alongside
[`paper_ex`](https://hex.pm/packages/paper_ex) and friends), but it has
no dependency on any of the others and works with any bot.
## Two layers
- **Controls** (`PolymarketNotify.Controls`) — the core: a pure data
model of per-**category** defaults (`:fills`, `:resolutions`,
`:errors`, `:daily_summary`, `:consensus`) with per-**strategy**
overrides. Route `fills` for the AI strategy to Telegram while muting
`fills` for the surge bots, apply a `min_value` threshold, etc. Pure
data + pure resolution — no I/O, trivially testable, and a UI/editor
can read and write it later.
- **Channels** (`PolymarketNotify.Channel`) — pluggable delivery. A
channel is any module implementing the `deliver/2` behaviour that
ships text somewhere. Built-ins: `Telegram`, `Discord`, `Webhook`,
`Log`, and a `Multi` fan-out. Bring your own by implementing the
behaviour.
## Design notes
- **Best-effort.** `PolymarketNotify.notify/3` consults the controls
and, if the `(category, strategy, value)` passes, fans the message
out. Nothing configured / not allowed ⇒ a silent no-op. It **never
raises into the caller** and always returns `:ok`; a channel that
errors or raises is isolated.
- **No process, no state.** The controls are plain maps you own and
persist however you like. The library adds no supervision tree.
- **Injectable HTTP.** Every network channel accepts an `:http`
function `(url, json -> {:ok, status} | {:error, term})`, so tests
need no real network and you can swap the client.
- **Secret-aware config.** Any option value may be given as
`{:system, "VAR"}` to read from the environment at delivery time
instead of baking a token into config.
## Installation
Add `polymarket_notify` to your deps in `mix.exs`:
```elixir
def deps do
[
{:polymarket_notify, "~> 0.1"}
]
end
```
## Usage
Configure channels and (optionally) a default control set:
```elixir
config :polymarket_notify,
channels: %{
telegram: {PolymarketNotify.Telegram,
[bot_token: {:system, "TELEGRAM_BOT_TOKEN"},
chat_id: {:system, "TELEGRAM_CHAT_ID"}]},
log: {PolymarketNotify.Log, []}
},
controls: PolymarketNotify.Controls.default()
```
Then notify from anywhere in your bot:
```elixir
# Simple: consults app config for controls + channels.
PolymarketNotify.notify(:fills, "Filled 100 @ 0.62 (BTC-up)")
# With a strategy tag (for per-strategy overrides) and a magnitude
# (for the category's :min_value threshold):
PolymarketNotify.notify(:fills, "Big fill!", strategy: "ai", value: 250.0)
# Inject controls/channels directly (handy in tests):
PolymarketNotify.notify(:errors, "boom",
controls: my_controls,
channels: %{log: {PolymarketNotify.Log, [level: :error]}})
```
### The control model
```elixir
%{
categories: %{
fills: %{enabled: true, min_value: 25.0, channels: [:telegram]},
resolutions: %{enabled: true, channels: [:telegram]},
errors: %{enabled: true, channels: [:telegram, :log]},
daily_summary: %{enabled: true, channels: [:telegram]}
},
# per-strategy overrides: tag => %{category => partial pref}
strategies: %{
"surge" => %{fills: %{enabled: false}},
"ai" => %{fills: %{enabled: true, min_value: 0.0}}
},
default_channels: [:log]
}
```
Resolution merges the category default with any per-strategy override
for that `(strategy, category)`. Start from `PolymarketNotify.Controls.default/0`
(every category on, routed to `default_channels`), or seed one entry
per strategy from a catalog:
```elixir
PolymarketNotify.Controls.from_catalog(["surge", %{tag: "ai"}, "my_custom_strat"])
```
### Writing a channel
```elixir
defmodule MyApp.SlackChannel do
@behaviour PolymarketNotify.Channel
@impl true
def deliver(message, opts) do
url = PolymarketNotify.resolve(Keyword.get(opts, :webhook_url))
# ... POST message, return :ok or {:error, reason}
:ok
end
end
```
## License
MIT. See [LICENSE](LICENSE).