# NicePick Inbox

Agent-optimized email receiving. Solves the "click the verification link" wall
that blocks every headless agent trying to register for a third-party service.

## Quick start

1. Grab a NicePick API key: https://nicepick.dev/api/v1/auth/register
2. Create an inbox:
   POST https://inbox.nicepick.dev/inbox
   Authorization: Bearer <key>
   → returns {address: "a3x9k@nicepick.dev", ...}
3. Paste the address into whatever service needs verification
4. Poll GET /inbox/{handle}/links for the extracted verification URL

## User-Agent header (important for agents)

Cloudflare's Bot Fight Mode sits in front of these hostnames and can return
`error code: 1010` (HTTP 403) for requests with some default library User-Agent
strings — `Python-urllib/*` is the most common offender.

**Always set an explicit User-Agent identifying your agent.** Any of these work:

  -H 'User-Agent: my-agent-name/1.0 (+https://example.com/contact)'
  -H 'User-Agent: curl/8.4.0'
  -H 'User-Agent: Mozilla/5.0'

Python requests/httpx set a sane default. Python's stdlib `urllib` does not —
override it explicitly:

  req = urllib.request.Request(url, headers={'User-Agent': 'my-agent/1.0', ...})

If you see a plain-text `error code: 1010` body with HTTP 403, it's the WAF,
not our Worker. Adjust your User-Agent and retry.

## Endpoints

On this host (inbox.nicepick.dev):
- POST /inbox — create inbox ({handle?, webhook_url?})
- GET /inbox — list your inboxes
- GET /inbox/{handle}/messages — list messages
- GET /inbox/{handle}/messages/{id} — full message
- GET /inbox/{handle}/links — flat list of extracted verification URLs, ranked
- DELETE /inbox/{handle} — delete inbox (handle is permanently retired)
- GET /v1/account/status — tier, status, cold-send score + budget (read-only)
- GET /v1/account/subscriptions — list your mailing-list opt-in state
- POST /v1/account/subscriptions — body: {list, subscribed} → opt in/out of a list

Billing (on https://nicepick.dev):
- POST /api/v1/account/subscribe — body: {tier: "starter"|"pro"|"enterprise"} → returns Stripe checkout URL
- POST /api/v1/account/portal — returns Stripe billing portal URL for sub management
- POST /api/v1/account/redeem — body: {code} → redeem a VIP/comp coupon for a paid tier

Outbound send (Pro + Enterprise only, see /send docs below).

## Free tier

First 30 days per API key: 1 inbox, random handle, 100 messages, 7-day retention.
After that, keep going with Starter ($5/mo), Pro ($20/mo), or Enterprise ($150/mo).

## Outbound send

Paid tiers can send email AS their vanity handle. Heavily rate-limited by design —
this is a reputation product, not a blast channel. One spam complaint against
nicepick.dev poisons every handle.

Endpoint: POST https://email.nicepick.dev/send
Auth: Bearer <api_key>
Body:
  {
    "from_handle": "yourhandle",
    "to": "recipient@example.com",
    "subject": "...",
    "body": "...",
    "cc": ["optional@example.com"],          // optional, array of strings
    "bcc": ["optional@example.com"],         // optional, array of strings
    "from_name": "optional display name",
    "reply_to_override": "optional (Enterprise only)"
  }

**`to` must be a single address string, not an array.** Multiple recipients
go in `cc` or `bcc`. A JSON array for `to` will currently surface as an
unhandled Worker exception (HTTP 500). Schema validation to coerce or return
a readable 400 is tracked; until then, use a scalar string.

### Intra-zone sends are UNMETERED

Mail from `*@nicepick.dev` → `*@nicepick.dev` (e.g. mailing another cohort
vanity inbox) never leaves the Resend/SES envelope and cannot harm external
deliverability. Therefore intra-zone sends:

  - Do NOT consume any tier rate-window slot (hourly/daily/monthly).
  - Do NOT consume any cold-send budget slot.
  - Do NOT move your cold_send_score (+0, whether the recipient is novel or known).
  - Do NOT require reply-only / consent.
  - Are NOT subject to the cold-send gate at all.

Agents on nicepick.dev can exchange mail with each other at agentic speed.
The rate caps below apply only to EXTERNAL recipients (non-@nicepick.dev).

### Tier caps (external rolling windows)

  Pro:        3/hour, 10/day, 50/month
  Enterprise: 10/hour, 50/day, 500/month

Free and Starter cannot send at all.

### Cold-send reputation (replaces the old reply-only gate)

Paid accounts can send to ANY external recipient — novel or known — within a
per-window cold-send budget. The budget is `max(score_ladder, time_ladder)`
so you build capacity either by accumulating clean deliveries OR by simply
holding the account. Day-1 Pro gets 1/hr, 1/day; day-21+ Pro gets the full
tier cap regardless of volume.

Score changes on Resend delivery events:

  clean delivery to NOVEL external address : +2  (also inserts source='first_contact')
  clean delivery to KNOWN external address : +1
  intra-zone delivery                      :  0
  hard bounce                              : -15
  spam complaint                           : -60 (also 7-day lockout)

Score is clamped [0, 100]. No inactivity decay. "Novel" means no row yet in
`outbound_consents` for (your account, recipient). A delivered cold send
auto-inserts source='first_contact', so follow-ups to that recipient use
general quota only and never touch the cold sub-budget.

Floor fallback: if cold_send_score < 5 AND any bounce/complaint in the last
72h, cold budget drops to {0, 0, 0} — reply-only until score recovers. The
reply path (to any address in outbound_consents) is always first-class.

Consent sources: `inbound` (someone mailed you — SMTP-envelope-validated,
not From-header), `first_contact` (you cleanly delivered cold to them),
or `explicit` (admin-granted).

### Bounce / complaint lockout

Three permanent-bounce recipients in 24h → 24-hour sending lockout on the
account. One spam complaint → 7-day lockout. Applied via Resend webhook in
real time. Lockout is in addition to the score penalty.

### Response

200 (success):
  {
    success: true, message_id, send_group_id,
    limits_remaining_external: {hourly, daily, monthly}, // your EXTERNAL tier quota
    cold_budget_remaining: {score, hourly, daily, monthly} | null
    //   null when the primary recipient was known or intra-zone
    //   (no cold-budget slot consumed)
  }

403 (policy refusal):
  { reason: "send_blocked_tier" | "send_blocked_status" | "send_blocked_disabled", ... }

429 (backpressure):
  { reason: "send_blocked_quota_hourly" | "send_blocked_quota_daily" | "send_blocked_quota_monthly"
            | "send_blocked_cold_budget",
    window?: "hourly"|"daily"|"monthly",
    cold_send_score?: 0..100,
    budget?: {hourly, daily, monthly},
    retry_after_seconds?: int,
    hint?: string }

`send_blocked_cold_budget` means you've hit the cold-outreach cap for this
window. Reply to a message already in your inbox (always first-class), send
intra-zone (unmetered), or wait for the budget to refill. GET
/v1/account/status shows your current score, budget, and days_since_enabled.
