# Export your application's usage events as CSV

Turn your own JSONL records into a minimal spreadsheet report without treating it as a platform bill.

## Choose the records you control

This recipe exports the application's own usage-events.jsonl file from the [user-tracking recipe](/en/cookbook/administration/user-tracking/). It creates a CSV for operational review. It is not an invoice, a complete MandAPI account export or a reconstruction of actual charges.

Use your account usage and statistics views to check platform activity. This script works only with your own application log; it does not use a platform export button or public export endpoint. Check actual account credits in the [MandAPI dashboard](https://app.mandapi.com/console).

## Save and try the exporter

Save this as export_activity.py. Python's standard library is sufficient. Start with --demo; it reads no account data and performs no network requests.

```python
import argparse
import csv
from datetime import datetime
import json
from pathlib import Path

FIELDS = ("record_type", "time", "request_id", "feature", "model", "status",
          "prompt_tokens", "completion_tokens", "duration_ms")
NUMBERS = {"prompt_tokens", "completion_tokens", "duration_ms"}

def safe_text(value):
    if not isinstance(value, str) or len(value) > 200:
        raise ValueError("Expected a short text field")
    text = value.replace("\r", " ").replace("\n", " ")
    return "'" + text if text.lstrip().startswith(("=", "+", "-", "@")) else text

def clean(event):
    if not isinstance(event, dict) or event.get("status") not in {"ok", "error"}:
        raise ValueError("Invalid event status")
    timestamp = datetime.fromisoformat(event["time"])
    if timestamp.tzinfo is None:
        raise ValueError("Timestamp must include a time zone")
    row = {}
    for field in FIELDS:
        if field == "record_type":
            row[field] = "application_usage_not_billing"
        elif field in NUMBERS:
            value = event.get(field)
            if value is not None and (type(value) is not int or value < 0):
                raise ValueError("Token and duration fields must be nonnegative integers")
            row[field] = "" if value is None else value
        else:
            row[field] = safe_text(event.get(field, ""))
    return row

parser = argparse.ArgumentParser()
parser.add_argument("source", nargs="?")
parser.add_argument("--output", default="usage-activity.csv")
parser.add_argument("--demo", action="store_true")
args = parser.parse_args()
if args.demo:
    events = [
        {"time": "2026-10-30T09:00:00+00:00", "request_id": "demo-1", "feature": "support", "model": "MODEL_ID", "status": "ok", "prompt_tokens": 12, "completion_tokens": 5, "duration_ms": 400},
        {"time": "2026-10-30T09:01:00+00:00", "request_id": "demo-2", "feature": "=2+2", "status": "error", "duration_ms": 200, "api_key": "DROP_THIS_FIELD"},
    ]
else:
    if not args.source:
        parser.error("Provide your own usage-events.jsonl or choose --demo")
    with Path(args.source).open(encoding="utf-8") as handle:
        events = [json.loads(line) for line in handle if line.strip()]
rows = [clean(event) for event in events]
with Path(args.output).open("x", newline="", encoding="utf-8-sig") as handle:
    writer = csv.DictWriter(handle, fieldnames=FIELDS)
    writer.writeheader()
    writer.writerows(rows)
print(json.dumps({"rows": len(rows), "output": args.output, "billing_report": False}))
```

```powershell
python export_activity.py --demo --output usage-demo.csv
```

The demo writes two rows. The failed request has blank token cells because its usage is unknown; blank is different from zero. Its extra api_key field is discarded by the allowlist, and a formula-like feature value is written as text.

The output file must not already exist. Review spreadsheets in the program you use: prefixing formula-like text is a basic protective measure, not a replacement for careful handling of untrusted spreadsheets.

## Export your own application log

Use a local copy containing only records you are entitled to process. The earlier tracking example uses timezone-aware timestamps, one application request ID, status and any returned token usage.

```powershell
python export_activity.py usage-events.jsonl --output usage-activity.csv
```

Only listed columns are exported. The script omits prompts, answers, keys, user identifiers and arbitrary extra fields. It rejects invalid status, timezone-free timestamps and negative or noninteger token counts. It does not change the source log.

For a large service, export an authorized, bounded query from your application's database instead of loading an unbounded log file. Define who may download the report and how long it is retained.

## Reconcile and check the result

Check row counts against the chosen source interval. Missing events, retries, streaming usage gaps and requests from other applications can make this report incomplete. A model token count does not by itself establish the credit deducted.

The demo CSV must contain exactly two data rows, a fixed application_usage_not_billing marker and blank usage for the failed request. Confirm the unexpected api_key field is absent. Use actual dashboard records to investigate account billing differences; do not infer money totals by treating unknown usage as zero.

## References

- [https://openrouter.ai/docs/cookbook/administration/activity-export](https://openrouter.ai/docs/cookbook/administration/activity-export)
- [https://docs.python.org/3/library/csv.html](https://docs.python.org/3/library/csv.html)
- [https://app.mandapi.com/console](https://app.mandapi.com/console)
