# time2live

> Scheduling and liveness (TTL) service for autonomous AI agents. Cron or one-off HTTP webhook jobs (signed, retried) and heartbeat monitors — a dead man's switch that alerts by webhook or Telegram when an agent stops pinging. Agents register with an EVM wallet and pay with x402 (USDC on Base); no human account needed.

**Network: Live — real payments on Base mainnet.**

**Open source (AGPL-3.0):** the full codebase — API, worker/keeper and the on-chain contracts — is public at https://github.com/MeMikko/TTL. The deployed contract is verifiable against the tagged source (`audit-v1`); the contract is unaudited (packet at `contracts/audit/SCOPE.md`, auditor being engaged). Self-host or run a Base Sepolia testnet instance from `docs/OPERATIONS.md` (§8d). Follow updates at https://x.com/t2lxyz.

Base URL: https://time2live.xyz. JSON over HTTPS; errors are `{"error":{"code","message"}}`. Auth: `Authorization: Bearer t2l_…`. Every create call accepts `Idempotency-Key`.

**Rate limits:** requests are limited per client IP and per API key; the limit, remaining and reset are returned in the response headers, and an over-limit call returns `429` with `Retry-After`. Auth endpoints have a tighter per-IP limit.

**Sign-in chain:** `chainId` in the challenge is the EIP-4361 chain that binds the signature. It is **independent of the x402 payment network** — sign in with any accepted Base chain (8453 mainnet or 84532 Sepolia); payment always settles on this instance's configured network (see **Network** above).

## Quickstart (curl)

1. `POST /v1/auth/challenge` `{"address":"0x…","chainId":8453}` → `message` (chainId: 8453 or 84532)
2. Sign `message` verbatim with the wallet (EIP-191 personal_sign; smart wallets via ERC-1271/6492 work).
3. `POST /v1/auth/verify` `{"message","signature"}` → `apiKey.key` (shown once).
4. Heartbeat: `POST /v1/monitors` `{"name":"agent-1","ttlSeconds":300}` → `pingUrl`; then `curl -fsS -X POST <pingUrl>` more often than every 300 s. Missing pings → `monitor.down` alert.
5. Job: `POST /v1/jobs` `{"name":"wake","schedule":{"type":"cron","expression":"*/15 * * * *"},"target":{"url":"https://agent.example.com/wake"}}`. Deliveries carry `T2L-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">`; the secret comes from `GET /v1/account/webhook-secret`.

## Webhooks & signatures

- Every delivery is signed: `T2L-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "${t}.${rawBody}")>`. Verify against the raw body, and **reject timestamps older than 5 minutes** to stop replays.
- Get the secret with `GET /v1/account/webhook-secret`; rotate it with `POST /v1/account/webhook-secret/rotate` (effective immediately — the old secret stops validating).
- Delivery is **at-least-once**: retried with exponential backoff on failure, so make your handler idempotent (each run has a stable id). Every delivery carries `T2L-Delivery-Id` (stable across retries — dedupe on it), `T2L-Attempt`, and `T2L-Scheduled-For` (the slot's ISO time). Targets must be public HTTPS; redirects are not followed and private/loopback/metadata addresses are blocked, also after DNS resolution.
- **Missed slots after downtime are collapsed, not replayed:** at most one catch-up run per job fires, then the schedule jumps to the next future slot — never a backlog burst. Set `freshnessSeconds` on a job to drop even that one catch-up when its slot is older than the cutoff (recorded `skipped`, not delivered); omit it to deliver regardless of lateness. You can also gate client-side on `T2L-Scheduled-For`.
- Lost or leaked an API key? `POST /v1/account/keys/revoke-all` cuts every key at once (the wallet operator session keeps working).

## Liveness receipts

- `GET /v1/monitors/{id}/receipt` returns a **server-signed (Ed25519) liveness receipt**: a portable attestation an agent can hand to a third party to *prove* its state rather than pointing at a dashboard. It records the schedule id, last heartbeat + success hash, the missed-window rule, the stop/alert action, and — crucially — distinguishes `halted_by_operator` (paused on purpose) from `missed_window` (went silent).
- It also carries **custody, not just decay**: `nextAllowedAction` lists, for the current liveness, who may restart/pause/escalate and the proof they need. A `halted_by_operator` monitor is the operator's to resume (a ping records the time but does not un-halt it); a `missed_window` one re-arms on the next heartbeat (the secret ping URL, or a passing active probe); escalation is always automatic on a miss to the stopAction channels.
- Verify offline: Ed25519 over the canonical JSON of `receipt` (object keys sorted recursively, no whitespace), against the public key at [`/.well-known/time2live-receipts.json`](https://time2live.xyz/.well-known/time2live-receipts.json).

## MCP

- [Remote MCP server](https://time2live.xyz/mcp): Streamable HTTP, stateless. Tools: register_challenge, register, get_status, create_job, list_jobs, trigger_job, delete_job, create_heartbeat, list_monitors, ping, activate, buy_credits.
- [Server card](https://time2live.xyz/.well-known/mcp/server-card.json)
- Client config: `{"mcpServers":{"time2live":{"type":"http","url":"https://time2live.xyz/mcp","headers":{"Authorization":"Bearer t2l_…"}}}}`

## Pricing (x402, eip155:8453)

- Unactivated: 1 monitor + 50 runs/month. Activation $0.10 once → 3 monitors + 100 runs/month.
- Beyond that, from prepaid credits: $0.000500 per run, $0.25 per extra monitor per 30 days. Packs: $1, $5, $20.
- A call that needs payment returns `402` with a `PAYMENT-REQUIRED` header (x402 v2). Pay with an x402 client and retry the same request with `PAYMENT-SIGNATURE`; the receipt is in `PAYMENT-RESPONSE`. Over MCP the tool result carries PaymentRequired and the client retries with `_meta["x402/payment"]`.


## On-chain dead man's switch (Base, chain 8453)

For funds, not just alerts: `DeadMansSwitchFactory` at `0x3D7cE7C30b712bC070Ba1ea2918Bd2211AD4349A`. `createSwitch(agent, beneficiary, ttl, tokens[], salt)` (payable) deploys your own switch holding ETH and up to 20 ERC-20s. The agent or owner calls `ping()` at least every `ttl` seconds (1 h – 365 d); after the deadline anyone can call `trigger()` and everything goes to the beneficiary. Our keeper calls `trigger()` automatically. Before the deadline only the owner can withdraw; after it, nobody can stop the transfer. The owner can change the beneficiary while the switch is live; once the deadline passes it is locked. **The contract is unaudited** — read the verified source on the block explorer before depositing.

## API reference

- [OpenAPI 3.1](https://time2live.xyz/openapi.json): every endpoint, schema and error.
- [Health](https://time2live.xyz/healthz): `?deep=1` also checks the database and worker.

## Optional

- Monitor states: new → alive → dead (after ttlSeconds + graceSeconds without a ping) → alive; paused. Alerts: monitor.down, monitor.up, monitor.unpaid.
- Monitor modes: `heartbeat` (default — your agent pushes pings) or `active` — we probe your own URL from the outside every `check.intervalSeconds`, and a 2xx counts as the ping. Active checks travel the same path a real request does, so a process can't report healthy from inside while its front door is closed. Create with `{"mode":"active","check":{"url":"https://you/health","intervalSeconds":60},"ttlSeconds":300}`.
- Active `check.expect` assertions go beyond a 2xx: `status` (exact code), `bodyContains` (a substring, defeating a hollow 200), and a **freshness** check — `jsonPath` (a dotted path like `data.updatedAt`) plus `maxAgeSeconds`, so a stale-but-correct cached 200 fails when its timestamp is older than the cutoff. Timestamps may be ISO-8601 or unix seconds/ms; the body must be JSON within the first ~4 KB.
- Job targets must be public HTTPS (private, loopback and metadata addresses are blocked, also after DNS resolution); redirects are not followed. Retries with exponential backoff; history kept 30 days.
