Envelopay Spec v0.2.0

Certified Mail argued for the protocol. Sent demonstrated it. This is the reference.

Protocol

Every envelopay message is an email. One thing is required:

The subject is the protocol. A bare WHICH with no body is valid. The X-Envelopay-Type header is optional — agents parsing via API can use it for routing, but most email clients don’t surface custom headers.

Subject parsing. The first token of the subject must be one of the nine all-caps keywords: WHICH, METHODS, PAY, ORDER, FULFILL, INVOICE, OFFER, ACCEPT, OOPS. No stripping — Re: and Fwd: prefixes mean the sender doesn’t speak the protocol. If the subject and JSON body disagree on type, the subject wins — reply OOPS if the mismatch matters.

When a JSON body is present, it must be a single JSON object containing v (version string) and type (lowercase type name). Unknown fields must be ignored. The email may contain signatures, rich text, HTML wrappers, or other MIME parts — the protocol is agnostic. Extraction of the JSON object from the message body (stripping signatures, HTML tags, quoted text) is the receiver’s responsibility.

Natural language mode. A METHODS reply with no parseable JSON body is valid. If the receiver replies to WHICH with a correctly formatted subject line (METHODS | ...) and natural language in the body describing accepted rails, accepts_natural_language is implicitly true. The sender should extract payment details (chain, token, wallet, price) from the natural language and may use natural language in subsequent ORDER tasks and notes. When JSON is present and accepts_natural_language is explicitly set, the explicit value takes precedence.

Assets. The chain + token pair is the asset identity. USDC on Solana and USDC on Base are different assets. Symbols (SOL, USDC) and contract addresses are both valid as token; chain disambiguates.

Settlement model. For PAY, ORDER (prepaid), and OFFER, payment moves before the email is composed — the email carries a proof that it already happened. ACCEPT is the same: the counter-payment moves first, then the email carries the proof. INVOICE and WHICH carry no proof; they are requests. The protocol transports proofs, requests, and work products. It does not touch, hold, or verify funds. Proof structure is rail-defined and opaque to the protocol.

Identifiers. Every message type should carry an id (sender-generated, opaque — how you generate it is an application concern, unique within sender namespace). Messages reference specific prior messages with typed refs: order_ref, invoice_ref, offer_ref, which_ref. For errors or other cross-type references, OOPS may include a generic ref field with the id of any message.

Threading. ORDER↔FULFILL, INVOICE↔PAY, and OFFER↔ACCEPT pairs should preserve email threading via In-Reply-To and References. Other reply types (METHODS, OOPS) may thread but are not required to.

DKIM. Senders should DKIM-sign all envelopay messages. Receivers should verify when available. DKIM is not a prerequisite — forwarders and legitimate setups may lack it.

Nine message types

TypeDirectionPurpose
WHICHA → B”What do you accept?”
METHODSB → AAccepted rails, wallets, pricing
PAYA → BPayment proof, no task
ORDERA → BTask request, optionally prepaid
FULFILLB → AWork product
INVOICEB → A”You owe me this, here’s my wallet”
OFFERA → B”I’ll give you X for Y” + proof
ACCEPTB → A”Deal” + counter-proof
OOPSeitherSomething went wrong

How to send each one

WHICH

Ask what the receiver accepts.

To: agent@example.com
Subject: WHICH

Optionally include a task for pricing:

{"v":"0.2.0",
 "type":"which",
 "id":"wch_1a2b",
 "note":"Looking for a security-focused code review",
 "task":{"description":"Review PR #417"}}

Expected response: METHODS. If you already know the receiver’s wallet, skip to ORDER or PAY.

FieldRequiredDescription
idnoSender-generated identifier
notenoHuman-readable context
tasknoTask description for pricing

METHODS

Reply with what you accept. Quotes are indicative, not binding — METHODS is cheap.

Subject: METHODS | $0.50 USDC, Solana preferred
{"v":"0.2.0",
 "type":"methods",
 "id":"mth_3c4d",
 "which_ref":"wch_1a2b",
 "note":"$0.50 USDC, Solana preferred",
 "accepts_natural_language": true,
 "rails":[
   {"chain":"solana",
    "token":"SOL",
    "wallet":"6dL6n77jJFWq4bu3cQp57H8rMUPEXu7uYN1XApPxpUif",
    "price":"500000000"},
   {"chain":"base",
    "token":"USDC",
    "wallet":"0x1a2B...",
    "price":"500000"},
   {"chain":"stripe",
    "token":"USD",
    "wallet":"https://pay.stripe.com/c/cs_live_abc123",
    "price":"50"}
 ]}
FieldRequiredDescription
railsyesAt least one accepted rail. Fiat rails (Stripe, Interac, PayPal) use chain for the network and wallet for the payment address or URL. Each rail has chain, token, wallet, and indicative price (smallest unit string, same convention as amount)
accepts_natural_languagenotrue if the receiver accepts natural language in ORDER tasks and notes. Default false — sender should use structured task objects. Implicitly true when the METHODS reply has no parseable JSON body
idnoSender-generated identifier
which_refnoThe WHICH id this responds to
notenoHuman-readable summary

PAY

Send money. No task. No reply expected.

To: friend@example.com
Subject: PAY | Dinner split
{"v":"0.2.0",
 "type":"pay",
 "id":"pay_5e6f",
 "note":"Dinner — my half",
 "amount":"30000000",
 "token":"USDC",
 "chain":"base",
 "proof":{"tx":"0x7a3f..."},
 "invoice_ref":"inv_9mN3"}
FieldRequiredDescription
idyesSender-generated identifier
amountyesAmount in smallest unit (string)
tokenyesAsset symbol (SOL, USDC) or contract address
chainyesSettlement chain
proofyesRail-specific evidence (tx hash, signed intent, etc.) — structure is rail-defined
invoice_refnoThe INVOICE id this pays
notenoHuman-readable context

ORDER

Request work. Can be unpaid (worker replies INVOICE or FULFILL) or prepaid (payment proof included, worker replies FULFILL directly).

Unpaid order — the worker decides what to charge:

To: worker@example.com
Subject: ORDER | Review PR #417
{"v":"0.2.0",
 "type":"order",
 "id":"ord_4vJ9",
 "note":"Review PR #417, focus on auth boundaries",
 "task":{"description":"Review PR #417",
         "repo":"github.com/alice/widget",
         "scope":"security"}}

Prepaid order — payment proof included, worker fulfills directly:

To: books@shop.com
Subject: ORDER | The Encrypted Commons, epub
{"v":"0.2.0",
 "type":"order",
 "id":"ord_8xK2",
 "note":"The Encrypted Commons, epub format",
 "task":{"description":"The Encrypted Commons, epub"},
 "amount":"8000000",
 "token":"USDC",
 "chain":"base",
 "proof":{"tx":"0x9c4e..."}}
FieldRequiredDescription
idyesSender-generated identifier
taskyesWhat needs to be done
amountnoPayment amount in smallest unit (string). Present when prepaid
tokennoAsset symbol or contract address. Required when amount is present
chainnoSettlement chain. Required when amount is present
proofnoPayment proof. Required when amount is present
notenoHuman-readable context

When amount and proof are present, the ORDER is prepaid. The receiver verifies the proof and replies with FULFILL. When absent, the receiver replies with INVOICE, FULFILL (if free), or OOPS.

FULFILL

Deliver the work. Should reply to the ORDER email via In-Reply-To.

Subject: FULFILL | Approved with 2 comments
{"v":"0.2.0",
 "type":"fulfill",
 "id":"ful_7g8h",
 "order_ref":"ord_4vJ9",
 "note":"Approved with 2 comments, one medium severity",
 "result":{"summary":"Approved with 2 comments",
           "findings":[{"file":"handler.go","line":47,
                        "severity":"medium",
                        "finding":"Session token not validated before use"}]}
}
FieldRequiredDescription
idyesSender-generated identifier
order_refyesThe ORDER id this fulfills
resultyesWork product
notenoHuman-readable summary

INVOICE

Bill someone. Expected response: PAY.

To: client@example.com
Subject: INVOICE | Additional auth hardening
{"v":"0.2.0",
 "type":"invoice",
 "id":"inv_9mN3",
 "order_ref":"ord_4vJ9",
 "note":"Auth hardening beyond original scope",
 "amount":"1000000",
 "token":"SOL",
 "chain":"solana",
 "wallet":"6dL6n77jJFWq4bu3cQp57H8rMUPEXu7uYN1XApPxpUif",
 "due":"2026-04-15"}
FieldRequiredDescription
idyesSender-generated identifier
amountyesAmount owed in smallest unit (string)
tokenyesAsset symbol or contract address
chainyesSettlement chain
walletyesWhere to send payment
duenoISO 8601 date (a signal, not enforced)
order_refnoThe ORDER this relates to
notenoHuman-readable context

OFFER

Propose an exchange. The offerer moves their asset first, then sends the proof with what they want in return. Expected response: ACCEPT or OOPS. OFFER↔ACCEPT pairs should preserve email threading.

To: counterparty@example.com
Subject: OFFER | 1 SOL for 30 USDC
{"v":"0.2.0",
 "type":"offer",
 "id":"ofr_2k3m",
 "note":"1 SOL for 30 USDC",
 "give":{"amount":"1000000000","token":"SOL","chain":"solana",
         "to":"6dL6n77jJFWq4bu3cQp57H8rMUPEXu7uYN1XApPxpUif",
         "proof":{"tx":"4vJ9..."}},
 "want":{"amount":"30000000","token":"USDC","chain":"base"},
 "wallet":"0x1a2B..."}
FieldRequiredDescription
idyesSender-generated identifier
giveyesWhat was sent: amount, token, chain, recipient wallet (to), and proof
wantyesWhat is expected back: amount, token, chain
walletyesWhere to send the counter-asset
notenoHuman-readable context

ACCEPT

Complete an exchange. The accepter verifies the OFFER proof, moves the counter-asset, and sends proof. Should reply via In-Reply-To.

Subject: ACCEPT | 30 USDC sent
{"v":"0.2.0",
 "type":"accept",
 "id":"acc_4n5p",
 "offer_ref":"ofr_2k3m",
 "amount":"30000000",
 "token":"USDC",
 "chain":"base",
 "proof":{"tx":"0x8c7d..."}}
FieldRequiredDescription
idyesSender-generated identifier
offer_refyesThe OFFER id this accepts
amountyesCounter-payment amount (must match OFFER’s want.amount)
tokenyesCounter-payment asset (must match OFFER’s want.token)
chainyesCounter-payment chain (must match OFFER’s want.chain)
proofyesCounter-payment proof — structure is rail-defined
notenoHuman-readable context

OOPS

Something went wrong. The note tells a human; the error object tells an agent.

Subject: OOPS | Payment not found on-chain
{"v":"0.2.0",
 "type":"oops",
 "id":"oops_0i1j",
 "note":"Payment not found on-chain",
 "error":{"code":"tx_not_found","tx":"0x3a7f..."}}
FieldRequiredDescription
idnoSender-generated identifier
noteyesHuman-readable explanation
errornoMachine-readable error object with code
refnoThe id of the message this error relates to (any type)

Error codes: tx_not_found, amount_mismatch, dkim_failed, unknown_type, insufficient_funds, missing_wallet.

If the subject matches ^[A-Z]+(\s*\|.*)?$ but the keyword isn’t one of the nine types, reply OOPS with unknown_type and the list of supported types.

No message requires a response. Silence is always valid. OOPS is a courtesy.

Flows

Pay: PAY → done.

Prepaid order: ORDER (with proof) → FULFILL. Two emails.

Order work: ORDERINVOICEPAYFULFILL. Four emails.

Free work: ORDERFULFILL. Two emails.

Invoice: INVOICEPAY. Two emails.

First contact: WHICHMETHODSORDER (with proof) → FULFILL. Four emails.

First contact (unpaid): WHICHMETHODSORDERINVOICEPAYFULFILL. Six emails.

Repeat customer: ORDER (with proof) → FULFILL. Two emails.

Exchange: OFFERACCEPT. Two emails, two on-chain transfers.

Verification

Receivers should verify before acting on any proof-carrying message (PAY, ORDER with proof, OFFER, ACCEPT):

  1. DKIM signature (when available)
  2. Payment proof per the rail’s verification method (tx exists, amount matches, recipient matches)
  3. Replay protection (sender + protocol id + proof deduplication)

Example: AgentMail

Any email API works. Here’s AgentMail as one example.

# Send a prepaid ORDER
curl -X POST https://api.agentmail.to/v0/inboxes/me@agentmail.to/threads \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":["books@shop.agentmail.to"],
       "subject":"ORDER | The Encrypted Commons",
       "text":"{\"v\":\"0.2.0\",\"type\":\"order\",\"id\":\"ord_1\",\"task\":{\"description\":\"The Encrypted Commons, epub\"},\"amount\":\"8000000\",\"token\":\"USDC\",\"chain\":\"base\",\"proof\":{\"tx\":\"0x9c4e...\"}}"}'

# Reply to a thread
curl -X POST https://api.agentmail.to/v0/inboxes/me@agentmail.to/threads/$THREAD_ID/reply \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subject":"FULFILL | Done",
       "text":"{\"v\":\"0.2.0\",\"type\":\"fulfill\",\"id\":\"ful_1\",\"order_ref\":\"ord_1\",\"result\":{\"summary\":\"Done\"}}"}'

Receive via webhook: register a URL at AgentMail, incoming emails arrive as POST. Parse the JSON body, route by subject type.

What the protocol doesn’t do

ProtocolApplication
Message types and subject lineDiscovery and ranking
Proof payload (opaque)Proof verification per rail
Email threadingRetries and timeouts
DKIM (when available)Reputation and trust

Discovery, trust, escrow, disputes, refunds — application concerns. The protocol carries proofs. Applications decide policy.


Certified Mail — the argument | Sent — the demo | Repo | All Envelopay posts: june.kim/envelopay