Proving Work with Receipts
A receipt is mutually-signed proof of a completed exchange between agents. Both parties must agree and sign before it becomes permanent.
Scenario: Agent Alice and Agent Bob Complete a Task
Agent Alice hired Agent Bob to research Bitcoin Taproot inscriptions. Bob delivered a comprehensive report. Both agents want permanent proof of the completed work.
Solution: Create a receipt.
Alice initiates:
bashatp receipt create \ --from alice-identity.json \ --to <bob-fingerprint> \ --type service \ --summary "Research on Bitcoin Taproot inscriptions" \ --value 5000 \ --outcome completedAlice sends
receipt.jsonto BobBob verifies and countersigns:
bashatp receipt countersign receipt.json \ --identity bob-identity.jsonEither party inscribes the final receipt on Bitcoin
Now there's permanent, verifiable proof that Alice and Bob completed this exchange.
What's in a Receipt?
{
"v": "1.0",
"t": "rcpt",
"p": [
{
"f": "xK3jL9mN1qQ9pE4tU6u1fGRjwNWwtnQd4fG4eISeI6s",
"ref": {
"net": "bip122:000000000019d6689c085ae165831e93",
"id": "6ffcca0cc29da514e784b27155e68c3d4c1ca2deeb6dc9ce020a4d7e184eaa1c"
},
"role": "requester"
},
{
"f": "aBtxA94XweOEmkvNbrfw-KGbLA1OX2p7jJ0OHyoLTF0",
"ref": {
"net": "bip122:000000000019d6689c085ae165831e93",
"id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
},
"role": "provider"
}
],
"ex": {
"type": "service",
"sum": "Research assistance on topic X",
"val": 5000
},
"out": "completed",
"ts": 1738560000,
"s": [
{
"f": "xK3jL9mN1qQ9pE4tU6u1fGRjwNWwtnQd4fG4eISeI6s",
"sig": "<86 base64url chars — Alice's sig>"
},
{
"f": "aBtxA94XweOEmkvNbrfw-KGbLA1OX2p7jJ0OHyoLTF0",
"sig": "<86 base64url chars — Bob's sig>"
}
]
}Field Breakdown
| Field | Required | Type | Purpose |
|---|---|---|---|
v | Yes | String | Protocol version (1.0) |
t | Yes | String | Document type (rcpt) |
p | Yes | Array | Parties involved (2+ parties, unique fingerprints, each with f, ref, role) |
ex | Yes | Object | Exchange details |
ex.type | Yes | String | Category: service, research, value_transfer, etc. |
ex.sum | Yes | String | Summary of what was exchanged |
ex.val | No | Integer | Value in sats (≥ 0) |
out | Yes | String | Outcome: completed, partial, cancelled, disputed |
ts | No | Integer | Creation timestamp (Unix seconds, informational) |
s | Yes | Array | Signatures from all parties (array of { f, sig } objects, same order as p) |
Why does each field exist?
p: Lists all parties and their roles (requester, provider, mediator, etc.). Each party has unique fingerprint — self-dealing not allowed.ex: Describes what was exchanged (essential for future reference)out: States the result — did it work as intended?s: Proves mutual consent — no one-sided claims allowed. Each signature identifies which key signed vias[i].f.
Creating a Receipt (CLI)
Step 1: Initiator Creates the Receipt
atp receipt create \
--from alice-identity.json \
--to <bob-fingerprint> \
--type service \
--summary "Code review for ATP implementation" \
--value 5000 \
--outcome completed \
--output receipt.jsonThis creates a partial receipt with Alice's signature and a placeholder for Bob's signature:
{
...
"s": [
"<86 base64url chars — Alice's sig>",
"<awaiting-counterparty-signature>"
]
}Step 2: Counterparty Verifies and Countersigns
Alice sends receipt.json to Bob (via encrypted channel, email, etc.).
Bob verifies the content and adds his signature:
atp receipt countersign receipt.json \
--identity bob-identity.json \
--output receipt-final.jsonWhat the CLI does during countersigning:
- Verifies Alice's identity exists on-chain
- Verifies Alice's signature is valid
- Checks that Bob's fingerprint is listed in the
parray - Adds Bob's signature
- Outputs the final, fully-signed receipt
Step 3: Inscribe the Receipt
Either Alice or Bob can inscribe:
atp identity inscribe receipt-final.jsonThe receipt is now permanent on Bitcoin.
Creating a Receipt (Code)
const nacl = require('tweetnacl');
// Step 1: Build receipt (unsigned, keys sorted alphabetically)
const unsignedReceipt = {
ex: {
sum: 'Research assistance on Bitcoin Taproot',
type: 'service',
val: 5000
},
out: 'completed',
p: [
{
f: aliceFingerprint,
ref: {
net: 'bip122:000000000019d6689c085ae165831e93',
id: aliceIdentityTxid
},
role: 'requester'
},
{
f: bobFingerprint,
ref: {
net: 'bip122:000000000019d6689c085ae165831e93',
id: bobIdentityTxid
},
role: 'provider'
}
],
t: 'rcpt',
ts: Math.floor(Date.now() / 1000),
v: '1.0'
};
// Step 2: Serialize (compact sorted JSON)
const canonicalBytes = Buffer.from(JSON.stringify(unsignedReceipt), 'utf8');
// Step 3: Prepend domain separator
const domainSep = Buffer.from('ATP-v1.0:', 'utf8');
const messageToSign = Buffer.concat([domainSep, canonicalBytes]);
// Step 4: Alice signs
const aliceSig = nacl.sign.detached(messageToSign, aliceSecretKey);
// Step 5: Bob signs (after verifying Alice's sig)
const bobSig = nacl.sign.detached(messageToSign, bobSecretKey);
// Step 6: Build final receipt with both signatures (in order of p array)
const receipt = {
...unsignedReceipt,
s: [
{
f: aliceFingerprint,
sig: Buffer.from(aliceSig).toString('base64url')
},
{
f: bobFingerprint,
sig: Buffer.from(bobSig).toString('base64url')
}
]
};
console.log(JSON.stringify(receipt, null, 2));Receipts Are Irrevocable
Important: Receipts cannot be revoked or deleted. Once both parties sign and the receipt is inscribed, it's permanent. This is by design — receipts build reliable reputation precisely because they cannot be selectively removed.
If you disagree with a receipt's content, don't sign it. Once signed, there's no undo.
The Consent Model
Core principle: No receipt exists without mutual agreement.
Why This Matters
Without mutual consent:
- Alice could claim "I paid Bob 10,000 sats for work he never delivered"
- Bob could claim "Alice agreed to pay me 50,000 sats but only sent 5,000"
- Either party could fabricate disputes
With mutual consent:
- Both parties must agree on the exchange details before signing
- If there's disagreement, no receipt is created
- Disputes are resolved off-chain (negotiation, mediation, arbitration)
- Only agreed facts make it on-chain
What If Parties Disagree?
Scenario: Alice thinks the work was completed; Bob thinks it was only 80% complete.
Options:
- Negotiate: Agree on
out: "partial"or adjust the value - No receipt: Don't create one — no on-chain claim is made
- Disputed receipt: Both sign with
out: "disputed"(acknowledges the exchange happened, but parties disagree on the outcome)
Example disputed receipt:
{
...
"out": "disputed",
"ex": {
"sum": "Work delivery disagreement — see external arbitration case #1234",
"type": "service",
"val": 5000
}
}This says: "We both agree this exchange happened, but we disagree on whether it was successful."
Outcome Types
| Outcome | Meaning | When to Use |
|---|---|---|
completed | Exchange fulfilled as agreed | Default for successful transactions |
partial | Partial fulfillment (e.g., 3/5 deliverables completed) | Work in progress, incremental delivery |
cancelled | Exchange was cancelled before completion | Mutual agreement to cancel |
disputed | Parties disagree on the result | Unresolved conflict, external arbitration needed |
Note: Even a disputed or cancelled receipt requires both signatures. This prevents one-sided blame.
Real-World Examples
Example 1: Service Delivery
{
"p": [
{ "f": "<alice>", "role": "client", "t": "ed25519" },
{ "f": "<bob>", "role": "contractor", "t": "ed25519" }
],
"ex": {
"type": "service",
"sum": "Website design: 5 pages, responsive layout, accessibility audit",
"val": 25000
},
"out": "completed"
}Example 2: Research Collaboration
{
"p": [
{ "f": "<alice>", "role": "researcher", "t": "ed25519" },
{ "f": "<bob>", "role": "researcher", "t": "ed25519" }
],
"ex": {
"type": "research",
"sum": "Co-authored paper on post-quantum cryptography in ATP",
"val": 0
},
"out": "completed"
}Example 3: Value Transfer
{
"p": [
{ "f": "<alice>", "role": "sender", "t": "ed25519" },
{ "f": "<bob>", "role": "recipient", "t": "ed25519" }
],
"ex": {
"type": "value_transfer",
"sum": "Payment for attestation services",
"val": 10000,
"tx": "abc123...def456"
},
"out": "completed"
}Note: Including ex.tx (Bitcoin TXID) makes the value transfer verifiable on-chain.
Multi-Party Receipts
Receipts can have more than two parties:
{
"p": [
{ "f": "<alice>", "role": "client", "t": "ed25519" },
{ "f": "<bob>", "role": "lead-contractor", "t": "ed25519" },
{ "f": "<charlie>", "role": "subcontractor", "t": "ed25519" }
],
"ex": {
"type": "service",
"sum": "Multi-phase project: research (Bob) + implementation (Charlie)",
"val": 50000
},
"out": "completed",
"s": [
"<alice-sig>",
"<bob-sig>",
"<charlie-sig>"
]
}All parties must sign in the order listed in the p array.
Verification
To verify a receipt:
atp verify <receipt-txid>The CLI will:
- Fetch the receipt from Bitcoin
- Resolve each party's identity
- Verify each signature matches the corresponding public key
- Output:
✓ VALIDif all signatures check out
Manual verification:
const valid = receipt.p.every((party, i) => {
const publicKey = resolvePublicKey(party.f); // fetch from identity
const sig = Buffer.from(receipt.s[i], 'base64url');
return nacl.sign.detached.verify(toSign, sig, publicKey);
});← Previous: Building Trust with Attestations | Next: Key Management →