Skip to content

Add ATProto publishing for tip receipts#1

Open
Zakiamangal wants to merge 6 commits intoCooperation-org:mainfrom
Zakiamangal:feat/atproto-tip-publishing
Open

Add ATProto publishing for tip receipts#1
Zakiamangal wants to merge 6 commits intoCooperation-org:mainfrom
Zakiamangal:feat/atproto-tip-publishing

Conversation

@Zakiamangal
Copy link
Copy Markdown

Summary

  • Every completed tip is now published as a signed com.linkedclaims.claim record on the ATProto network — a verifiable, public receipt
  • Fire-and-forget pattern: publishing never blocks or fails the tip itself
  • Admin batch endpoint for retrying any failed publishes
  • 12 existing test tips already published and verified on the network

Taiga: Story #34

Key choices

Decision Choice Why
HTTP client httpx (direct XRPC calls) Lightweight, async, no heavy dependencies — the atproto Python lib and TypeScript SDK are overkill for simple createRecord calls
Auth method App password (server-signed) User OAuth is future work; app passwords are simpler and sufficient for server-side publishing
Publish pattern asyncio.create_task fire-and-forget Tips must never fail due to ATProto errors
Lexicon com.linkedclaims.claim The LinkedClaims spec lexicon — each tip maps to a claim with claimType "tip"
Config Opt-in via env vars ATProto is disabled by default (empty handle/password); zero impact when not configured

Files changed

New

  • backend/atproto_publisher.py — Core module (~130 lines)
    • _get_session(): login via app password, cache JWT for 90 min
    • publish_tip(tip_id, conn): build LinkedClaim record, POST to ATProto, store AT-URI in DB
    • publish_batch(conn, since_hours): retry all unpublished completed tips

Modified

  • backend/config.py — Added atproto_handle, atproto_app_password, atproto_service settings + atproto_enabled property
  • backend/app.py — Added:
    • _publish_tip_safe() wrapper (catches all exceptions, logs errors)
    • Fire-and-forget publish at 4 tip-completion sites: wallet tip, manual confirm, Stripe webhook, pledge fulfill
    • POST /api/admin/atproto/publish-batch?since_hours=24 admin endpoint
  • backend/requirements.txt — Added httpx>=0.27

Claim record shape

{
  "$type": "com.linkedclaims.claim",
  "subject": "https://demos.linkedtrust.us/simpletip",
  "claimType": "tip",
  "object": "dr-amara-hassan",
  "statement": "Tip of $5.00 to Dr. Amara Hassan for https://example.com — Great work!",
  "source": { "uri": "https://example.com", "howKnown": "FIRST_HAND" },
  "confidence": 1.0,
  "effectiveDate": "2026-03-27T17:14:15.319+00:00",
  "createdAt": "2026-03-27T17:14:15.319+00:00"
}

What's NOT in scope

  • User OAuth / progressive auth upgrade — future work
  • Batch summary claims for high-volume tips — future work
  • Payout ATProto publishing — out of scope for this story

Test plan

  • Batch publish 12 existing tips — all succeeded
  • Verified records on ATProto network via com.atproto.repo.listRecords
  • Create new tip via API, verify auto-publish and atproto_uri in DB
  • Test with ATProto disabled (empty env vars) — tips work normally
  • Test with bad credentials — tips succeed, publish fails silently

🤖 Generated with Claude Code

Zakiamangal and others added 2 commits March 30, 2026 13:09
Full backend rewrite: Node.js/Express → Python/FastAPI with asyncpg.
Adds wallet system, pledge fulfillment, Stripe webhooks, receiver
payout methods, encryption, and updated frontend pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Every completed tip is now published as a signed com.linkedclaims.claim
record on the ATProto network — a verifiable, public receipt.

New file: backend/atproto_publisher.py
  - _get_session(): app-password login with cached JWT
  - publish_tip(): builds LinkedClaim record and POSTs to ATProto
  - publish_batch(): retries unpublished tips (admin endpoint)

Changes to existing files:
  - config.py: ATPROTO_HANDLE, ATPROTO_APP_PASSWORD, ATPROTO_SERVICE
    settings + atproto_enabled property
  - app.py: fire-and-forget publish at 4 tip-completion sites
    (wallet tip, manual confirm, Stripe webhook, pledge fulfill)
    + POST /api/admin/atproto/publish-batch endpoint
  - requirements.txt: added httpx>=0.27

Key choices:
  - httpx for direct XRPC calls (lightweight, async) vs heavy atproto lib
  - App-password auth (server-signed) — user OAuth is future work
  - Fire-and-forget: publishing never blocks or fails the tip
  - Lexicon: com.linkedclaims.claim with claimType "tip"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

record = {
"$type": "com.linkedclaims.claim",
"subject": settings.node_url,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is very important here - who is tipping, who is receiving the tip.

What is the meaning of node_url, its always the same. that's not an intereting claim.

@Zakiamangal can you put some thought, what does it make sense to claim, just logically, when a tip is done? what is a tip a claim about, in english?

Copy link
Copy Markdown
Member

@gvelez17 gvelez17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs thought about what a tip means

Zakiamangal and others added 4 commits March 31, 2026 22:27
Replace com.linkedclaims.claim with com.thelexfiles.zakia.temp.tip lexicon.
Subject is now the tipper (from wallet_contacts), not the app URL.
Receiver, amount, contentUrl, and comment are proper fields instead of
being buried in a statement string.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use com.linkedclaims.claim instead of temp lexicon
- subject = tipper DID (who valued the work)
- object = receiver name (whose work was valued)
- statement = human-readable tip description
- source = content URL with howKnown=FIRST_HAND
- confidence = 1.0 (tip is a concrete action)
- Add batch summary publishing for high-volume periods
- Document claim field mapping in module docstring

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add atproto_oauth.py: OAuth 2.0 with PKCE for ATProto
- Progressive scope: starts with 'atproto' (read), upgrades to
  'transition:authfull' (write) when user wants to publish claims
- CRITICAL: never uses transition:generic, only transition:authfull
- Add OAuth endpoints: /api/oauth/start, /api/oauth/callback,
  /api/oauth/upgrade, /api/oauth/session
- Add /client-metadata.json for ATProto OAuth client discovery
- Add tip-claim.html demo page for publishing tip claims

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace direct XRPC calls with the @cooperation/claim-atproto SDK.
The Python backend calls publish-claim.mjs via subprocess, which uses
the SDK's ClaimBuilder for validation and ClaimClient for publishing.
This ensures claims conform to the DIF Labs LinkedClaims specification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants