postals — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited postals (Agent Skill) and scored it 96/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You have the ability to send physical handwritten postcards to US mailing addresses. A robotic pen writes the message in real ink on real paper, the card is stamped and handed to USPS, and it arrives in the recipient's mailbox in 3–5 business days. Each card costs $4.99.
This is not email. This is not digital. This is a physical object that shows up at someone's house.
Use send_postcard when:
Do not use this skill when:
send_postcard — mail one physical postcardRequired inputs: to_name, to_address, to_city, to_state (2-letter), to_zip, message (max 400 chars)
Optional inputs:
to_address_2 — apt/suite/unitfront_image_url — public HTTPS URL to the card's front image (PNG/JPEG, landscape 7×5 preferred). If omitted, uses the operator's configured default card.handwriting_style — font name. Default: "Joyful Jennifer"return_address — override the default sender address for this one sendidempotency_key — always include this for autonomous workflows. See the Idempotency section below.Every response includes:
success, order_id, card_id, recipient, estimated_deliverybalance_remaining (cents), cards_available, cards_sent_today, daily_limitUse the response fields to self-regulate. After each send, read cards_sent_today and daily_limit. If you're close to the limit, slow down or stop. In v1.1+, also read balance_remaining and cards_available to know when you're about to run out of funds.
generate_message — draft a personalized messageAlways call this before `send_postcard` unless the user already gave you the exact message to send. It produces a warm, personal, handwritten-style message tuned to Postals' proven tone profiles.
Inputs:
recipient_context (required) — who the recipient is and why you're writingtone — warm (default), professional, casual, or excitedsender_context — who's sendingmax_chars — default 350, hard max 400Two possible outputs:
{ message: "...", ai_generated: true } — use message directly in send_postcard.{ message: null, prompt: { system, user }, ai_generated: false } — you are an LLM. Read the system and user prompts, compose the message yourself following the guidelines, then pass that message to `send_postcard`.Don't surface the raw prompt to the human. Write the message yourself and pass it through.
compose_postcard_message (MCP prompt)Alternative to generate_message for zero-dependency message composition. Returns a prompt template with Postals' tone guidelines baked in. Mostly equivalent to generate_message with ai_generated: false.
For any single-card send:
1. Check the user's request — do they want physical mail? Confirm if ambiguous.
2. Collect or confirm recipient info (name, full US address).
3. If no message was provided: call generate_message with recipient context + tone + sender context.
4. Call send_postcard with the address, message, and (if applicable) front_image_url.
5. Include an idempotency_key.
6. Report the order_id and estimated delivery back to the user.
7. Read cards_sent_today / daily_limit / balance_remaining from the response.For a bulk/autonomous pipeline (e.g. pool builder workflow):
Loop over prospects:
1. Check cards_sent_today < daily_limit from the last response.
2. If you're at the limit, stop. Tell the human. Resume tomorrow.
3. In v1.1+, also check balance_remaining > cost_per_card. If not, stop and notify human.
4. Generate/fetch the personalized image URL for this prospect.
5. Call generate_message with that prospect's context.
6. Call send_postcard with a unique idempotency_key (e.g. "prospect-{id}").
7. Log the order_id.
8. Next prospect.If the tool returns an API key or balance error on the first send, you need to onboard the human. Follow this script:
If no API key is configured ("POSTALS_API_KEY not set..."):
"I'd love to send this postcard, but the Postals integration isn't set up yet. To enable it, you'll need to:
>
1. Get an API key at postals.ai/developers (takes about 2 minutes, costs $4.99/card) 2. Paste the key into your MCP server config as POSTALS_API_KEY 3. Restart this client>
Once you do that, I can send this card and any future ones without asking again. Want me to send this one as a draft email instead while you set it up?"
If wallet balance is empty (v1.1+, "Insufficient balance..."):
"My Postals wallet is empty — I've sent X cards since the last refill. To keep sending:
>
1. Add credits yourself at postals.ai/developers (credit card or crypto) 2. I can fund from my own wallet — currently [enabled / disabled by your settings] 3. You can send USDC or WLD directly to my Postals deposit address: [address]
>
Which would you prefer?"
If the daily limit is hit ("Daily limit reached..."):
"I've hit today's daily limit of N cards. The counter resets at midnight UTC. I'll queue this send for tomorrow. If you want to raise the limit, your operator can adjust POSTALS_DAILY_LIMIT in the MCP config."Remember the user's preference. Don't ask again on subsequent sends unless the configured path breaks.
Always include `idempotency_key` in `send_postcard` calls when running autonomously or in a loop.
An idempotency_key is a unique string you make up (e.g. "prospect-123-oak-st", "thank-you-for-client-abc", "campaign-X-recipient-456"). If a call with that key already succeeded in the last 24 hours, the server returns the original result without sending a duplicate card.
Without idempotency keys, a network timeout + retry = two postcards sent. With them, retries are safe.
Rule of thumb: one recipient + one purpose = one idempotency key. Don't reuse keys across unrelated sends.
The server enforces:
send_postcard calls. Don't try to beat this — just pace yourself. The delay is a firm floor.POSTALS_DAILY_LIMIT, default 50). Counter resets at midnight UTC. Once hit, all sends fail until reset.If you see a rate-limit or daily-limit error, it's not a bug. Respect the limit.
The generate_message tool already encodes these. But if you're composing yourself (via compose_postcard_message or the fallback prompt), follow these rules:
Tone profiles:
warm — like a friendly neighbor. Default for most use cases.professional — polished but personable. Business-appropriate.casual — relaxed, like texting a friend.excited — genuinely enthusiastic. Use sparingly.When the user has a specific image they want on the card front, use front_image_url. The URL must be:
For lead-gen workflows with personalized images (the pool builder pattern), the agent's image generation pipeline will produce a URL. Pass it directly.
If no image is provided and the operator didn't configure POSTALS_DEFAULT_CARD_IMAGE, send_postcard will return an error telling the user to configure one. Relay that to the human.
If the operator configured POSTALS_TEST_MODE=true, every send_postcard call validates inputs and returns a mock response with test_mode: true. No card is actually mailed and no money is spent.
You can tell you're in test mode because the response includes "test_mode": true and a mock_order_id prefixed with test-. If the user asks why their card didn't arrive and you see this flag, tell them:
"We're running in test mode — your operator hasPOSTALS_TEST_MODE=trueset. No cards are actually being mailed. To go live, have them remove that env var or set it tofalseand restart."
The tool returns isError: true with a plain-English message for common failures. When you see an error:
Never silently retry on errors. Always surface them to the human unless you can fix the root cause first.
After a successful send, a good report looks like:
"Done — mailed a handwritten postcard to Jane Smith at 123 Oak St, Austin TX. Order ID 196937. Should arrive in 3-5 business days. That was card 7 of your 25/day limit."
After hitting a limit:
"Sent 25 cards today and hit the daily cap. Pausing until midnight UTC. Want me to queue the remaining N prospects for tomorrow?"
After a critical error:
"Couldn't send — the fulfillment service rejected the address (possibly undeliverable). Here's the error: '...'. Want me to try a different address or skip this recipient?"
send_postcard as front_image_url.send_postcard.The operator (the human setting up your MCP config) controls:
POSTALS_DAILY_LIMIT)POSTALS_SENDER_*)POSTALS_DEFAULT_CARD_IMAGE)POSTALS_TEST_MODE)You cannot change these from inside the Postals tools themselves — send_postcard and generate_message execute actions, they don't reconfigure the server. But if you have filesystem access in your client (Claude Code, Cursor, Aider, Continue, etc.), you CAN edit the MCP config file on the user's behalf. See the next section for how.
Two scenarios here: (1) the user already has Postals loaded (you're reading this skill, so you're in that world) and wants to change a setting, or (2) the user wants Postals installed somewhere else — a teammate's machine, another of their own devices, etc.
When the user asks to change settings ("disable test mode", "raise my daily limit to 100", "update my return address", "add Cloudflare credentials", "change my handwriting style"), you can edit the MCP client config file directly if you have filesystem access.
Where the config lives:
| Client | Config path |
|---|---|
| Claude Desktop (macOS) | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Claude Desktop (Windows) | %APPDATA%\Claude\claude_desktop_config.json |
| Cursor (global) | ~/.cursor/mcp.json |
| Cursor (per-project) | .cursor/mcp.json in the project root |
| Claude Code | ~/.claude/settings.json — Postals lives under mcpServers.postals.env |
Workflow:
mcpServers.postals.envCommon changes and their env vars:
| User says | Env var to set |
|---|---|
| "Enable test mode" / "Turn on test mode" | POSTALS_TEST_MODE: "true" |
| "Disable test mode" / "Go live" | Remove POSTALS_TEST_MODE, or set to "false" |
| "Set my daily limit to N" | POSTALS_DAILY_LIMIT: "N" |
| "Update my return address" | POSTALS_SENDER_NAME, POSTALS_SENDER_ADDRESS, POSTALS_SENDER_CITY, POSTALS_SENDER_STATE, POSTALS_SENDER_ZIP |
| "Set my default card image" | POSTALS_DEFAULT_CARD_IMAGE: URL (HTTPS) |
| "Add AI message generation" | CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN |
| "Change my handwriting style" | POSTALS_HANDWRITING_STYLE: font name (e.g. "Joyful Jennifer") |
| "Rotate my API key" | POSTALS_API_KEY: new key value |
If the user asks you to set up Postals for a teammate, a remote server, or another of their devices — anywhere you don't have filesystem access — you can't directly install it. Instead, produce the exact config JSON they can hand off. Example response:
"I can't edit a config on another machine from here, but here's the exact snippet to add to their MCP config. Have them paste this into their client's config file (path depends on the client — Claude Desktop uses~/Library/Application Support/Claude/claude_desktop_config.jsonon macOS, Cursor uses~/.cursor/mcp.json, etc.) and restart:
>
``json { "mcpServers": { "postals": { "command": "npx", "args": ["-y", "postals-mcp"], "env": { "POSTALS_API_KEY": "<their_key>", "POSTALS_TEST_MODE": "true" } } } } ``If their machine has Claude Code or Cursor with filesystem access, the simpler alternative: tell the user to give their agent the same prompt: "Install the postals-mcp MCP server with my Postals API key XXX, test mode on."
Never log, echo back, or quote the user's API key in your responses. Reference it as "your API key" when describing changes. If the user pastes a key in plain text in the conversation, do not repeat the key back verbatim in subsequent messages. Treat POSTALS_API_KEY, CLOUDFLARE_API_TOKEN, and any other *_KEY / *_TOKEN value as a secret throughout the conversation.
Some config edits are effectively destructive:
send_postcard call from a mock response into a real $4.99 card mailed via USPSBefore making these specific changes, confirm the user's intent explicitly. Example: "Just to confirm — disabling test mode means the next `send_postcard` call will send a real card and cost $4.99 from your Postals wallet. Proceed?"
For reversible changes (return address, handwriting style, default image URL), no confirmation needed — just edit and tell the user to restart.
send_postcard call spends $4.99. Don't send accidentally. Confirm with the user if you're unsure.send_postcard, it's gone.mcpServers.postals.env directly — don't just tell them to do it themselves. You're the agent; this is your job.POSTALS_TEST_MODE to false means the next send costs real money. Always confirm intent before that change.If the user's request is ambiguous (e.g. "send something to John"), ask clarifying questions before calling the tool. A wasted $4.99 card is worse than a clarifying question.
If you're unsure whether to use send_postcard or some other skill, ask. Physical mail is the right tool for warm personal outreach, high-ROI lead gen, and memorable moments. It's the wrong tool for transactional notifications, urgent communications, or anything that needs to be fast or cheap.
TL;DR: One tool call (send_postcard), handwritten physical mail delivered in 3–5 days, $4.99 per card. Use generate_message or compose yourself. Always use idempotency_key in loops. Respect the daily limit. Read the response to self-regulate. Test mode for safe development.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.