@ssheleg/sheleg-dev 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -0
- package/package.json +5 -2
- package/plugins/sheleg-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/sheleg-dev/skills/ad-tracking/SKILL.md +30 -0
- package/plugins/sheleg-dev/skills/stripe-billing/SKILL.md +24 -0
- package/plugins/sheleg-dev/skills/stripe-billing/references/provider-concentration.md +120 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,82 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
Versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.5.1 — 2026-08-16
|
|
8
|
+
|
|
9
|
+
**This gate can now see an invariant it breaks one repository away.** The family umbrella
|
|
10
|
+
routes work by matching a prompt against a table in `lib/triggers.js`, and every trigger
|
|
11
|
+
there must be a word this skill's own `description` advertises. Nothing here knew that
|
|
12
|
+
table existed. On 2026-08-16 `sheleg-design` 1.37.0 shipped green having dropped a phrase
|
|
13
|
+
that was still a live trigger, the umbrella found out minutes after the tag, and it cost a
|
|
14
|
+
patch release — because the member releases FIRST and the umbrella re-pins after.
|
|
15
|
+
|
|
16
|
+
`test/validate.py` now asks the umbrella's own checker (`test/advertised_check.js`), which
|
|
17
|
+
reads the module the hook itself calls. **No copy of the table lives here**, so there is
|
|
18
|
+
nothing to drift. With no umbrella above this checkout — a standalone clone, and CI — it
|
|
19
|
+
discloses rather than passing, because a check that cannot look must never read as one
|
|
20
|
+
that looked.
|
|
21
|
+
|
|
22
|
+
Watched refusing a real drop before shipping: every one of the seven members carrying
|
|
23
|
+
routed triggers had one of its own advertised phrases removed and every one of them failed
|
|
24
|
+
its own gate.
|
|
25
|
+
|
|
26
|
+
## v0.5.0 — 2026-08-14
|
|
27
|
+
|
|
28
|
+
Two gaps found by reading a web2app funnel guide against the pack. Both are about
|
|
29
|
+
the same seam: the payment layer is where a funnel's money and its measurement
|
|
30
|
+
actually live, and both skills were treating it as somebody else's concern.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`stripe-billing` → `references/provider-concentration.md`.** The skill covered
|
|
35
|
+
dunning *inside* Stripe and never dependence *on* Stripe. The new reference is
|
|
36
|
+
about the seam rather than about adding a second provider: the entitlement keyed
|
|
37
|
+
to your own user id with provider ids as fields, the customer identity resolved
|
|
38
|
+
before checkout, and the reconciliation job as the only number in the system
|
|
39
|
+
that is not your own telemetry. `SKILL.md` → **Reconciliation** already excluded
|
|
40
|
+
"rows that were never Stripe's", which is that seam half-built and was never
|
|
41
|
+
named as one.
|
|
42
|
+
|
|
43
|
+
It also declines the obvious answer: a generic `PaymentProvider` interface
|
|
44
|
+
written against one provider encodes that provider's model and gets rewritten
|
|
45
|
+
for the second one anyway. The seam is in the data, not in the code shape.
|
|
46
|
+
|
|
47
|
+
**Automatic card updates are documented with their two limits**, both taken from
|
|
48
|
+
Stripe's own text rather than from recall: coverage varies by country, and Stripe
|
|
49
|
+
states it is **not possible to identify which cards support it** — so any plan
|
|
50
|
+
that assumes a reissue will be caught has an unmeasurable branch. Handle
|
|
51
|
+
`payment_method.automatically_updated` and `payment_method.updated`, write the
|
|
52
|
+
new expiry and last four to your own records, and review anything keyed on
|
|
53
|
+
`fingerprint`, which moves when the card number does.
|
|
54
|
+
|
|
55
|
+
And the distinction the pack had no field for: a subscription that ended because
|
|
56
|
+
someone chose to leave, versus one that ended because a bank reissued a number.
|
|
57
|
+
A single `canceled` status for both is how a recoverable failure gets a farewell
|
|
58
|
+
email.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **`ad-tracking` now says where a purchase event comes from.** Deduplication by
|
|
63
|
+
`event_id` was already covered; what was missing is the reason the server side
|
|
64
|
+
is not optional. Every other event on the list is something a person did in a
|
|
65
|
+
tab. A purchase is the outcome of a charge that succeeded inside a payment
|
|
66
|
+
system, and the only thing that knows it succeeded is the provider's webhook —
|
|
67
|
+
so firing it from the thank-you page fires it from the one place with no idea
|
|
68
|
+
whether the charge cleared, which is why a browser-only purchase count has an
|
|
69
|
+
event for every session that reached the page and a refund for none of them.
|
|
70
|
+
|
|
71
|
+
The ordering that follows: the webhook handler is the source of truth and sends
|
|
72
|
+
the server-side conversion; the browser event stays for the signals only it
|
|
73
|
+
carries and is subordinate on conflict; and everything the browser loses, it
|
|
74
|
+
loses closest to the money, so the bias is downward by an amount that cannot be
|
|
75
|
+
measured from inside the browser. Reconcile against the provider's count of
|
|
76
|
+
succeeded charges, which is the only external check on this event that is not
|
|
77
|
+
itself telemetry.
|
|
78
|
+
|
|
79
|
+
Both `SKILL.md` bodies stay inside the 500-line progressive-disclosure budget:
|
|
80
|
+
`stripe-billing` went to 502 lines when this was written inline, which is what
|
|
81
|
+
moved the full treatment into a reference and left a pointer behind.
|
|
82
|
+
|
|
7
83
|
## v0.4.3 — 2026-08-14
|
|
8
84
|
|
|
9
85
|
A red `validate` could not stop a publish, and this repository proved it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssheleg/sheleg-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Six integration skills for the layer a product reaches once it has users: Stripe subscription billing, crypto payments, ad and conversion tracking under Consent Mode v2, Google sign-in and the wider Google auth surface, and Core Web Vitals. This package is the installer CLI.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sheleg-dev": "bin/sheleg-dev.js"
|
|
@@ -41,5 +41,8 @@
|
|
|
41
41
|
"claude-code",
|
|
42
42
|
"cursor",
|
|
43
43
|
"skill"
|
|
44
|
-
]
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "python3 test/validate.py"
|
|
47
|
+
}
|
|
45
48
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "sheleg-dev",
|
|
3
3
|
"displayName": "Sheleg Dev",
|
|
4
4
|
"description": "The integration layer a product needs once it has users: Stripe subscription billing reconciled into your own database, crypto payments that survive over-payment and duplicate webhooks, ad and conversion tracking under Consent Mode v2, Google sign-in with the account-linking guards, the wider Google auth surface, and Core Web Vitals work that moves the score.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.5.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ssheleg",
|
|
8
8
|
"url": "https://x.com/sshlg93"
|
|
@@ -319,6 +319,36 @@ from both the browser and the server (Conversions API, server-side GA4) must
|
|
|
319
319
|
carry the SAME `event_id` / `transaction_id`, or both are counted. Test it by
|
|
320
320
|
reloading the thank-you page: a purchase that increments twice is a
|
|
321
321
|
deduplication bug, not a tracking success.
|
|
322
|
+
|
|
323
|
+
**A purchase is not a browser event, and that decides where it comes from.**
|
|
324
|
+
Every other event on the list is something a person did in a tab, so the browser
|
|
325
|
+
is its natural source. A purchase is the outcome of a charge that succeeded
|
|
326
|
+
inside a payment system, and the only thing that knows it succeeded is the
|
|
327
|
+
provider's webhook. Firing it from the thank-you page means firing it from the
|
|
328
|
+
one place that has no idea whether the charge cleared — which is why a purchase
|
|
329
|
+
event exists for every session that reached the page and a refund exists for
|
|
330
|
+
none of them.
|
|
331
|
+
|
|
332
|
+
The consequence is an ordering, not a preference:
|
|
333
|
+
|
|
334
|
+
1. **The webhook handler is the source of truth for the purchase.** It writes the
|
|
335
|
+
entitlement, and the same handler sends the server-side conversion. If the
|
|
336
|
+
handler is where the money is recorded, it is where the event belongs.
|
|
337
|
+
2. **The browser event stays, and it stays subordinate.** Keep it for the
|
|
338
|
+
attribution signals only the browser carries (click ids, the consent state,
|
|
339
|
+
the session), give it the `event_id` the server will reuse, and treat the
|
|
340
|
+
server as authoritative when they disagree.
|
|
341
|
+
3. **Everything the browser loses, it loses closest to the money.** Blockers, a
|
|
342
|
+
mobile browser terminating a tab on redirect, a payment provider's hosted page
|
|
343
|
+
ending the session before the return leg — each of them removes the *last*
|
|
344
|
+
step preferentially, so the browser-only purchase count is biased low by an
|
|
345
|
+
amount you cannot measure from inside the browser.
|
|
346
|
+
|
|
347
|
+
Reconcile against the provider rather than the analytics dashboard: the count of
|
|
348
|
+
succeeded charges in a period is a number the payment system will give you
|
|
349
|
+
exactly, and it is the only external check on this event that is not itself
|
|
350
|
+
telemetry. See `stripe-billing` → **Depending on one provider** for what else
|
|
351
|
+
that reconciliation surfaces.
|
|
322
352
|
## Content Security Policy
|
|
323
353
|
|
|
324
354
|
Read `references/performance-security.md` → **Content Security Policy** for the
|
|
@@ -43,6 +43,7 @@ Deep material, loaded on demand:
|
|
|
43
43
|
| [`references/subscription-lifecycle.md`](references/subscription-lifecycle.md) | implementing checkout, verify, renewal, seats, plan change, trials, cancellation, clawback |
|
|
44
44
|
| [`references/price-integrity.md`](references/price-integrity.md) | pricing lives in more than one file, or an advertised price must be proved against Stripe |
|
|
45
45
|
| [`references/testing-and-local-dev.md`](references/testing-and-local-dev.md) | local webhooks, fixtures, mocks, and a suite that would actually catch a money defect |
|
|
46
|
+
| [`references/provider-concentration.md`](references/provider-concentration.md) | growing revenue, opening a second market, separating involuntary churn, or asking what happens if the payment account is limited |
|
|
46
47
|
|
|
47
48
|
---
|
|
48
49
|
|
|
@@ -395,6 +396,29 @@ units: convert once, at the boundary, and compare in integer cents —
|
|
|
395
396
|
|
|
396
397
|
---
|
|
397
398
|
|
|
399
|
+
## Depending on one provider
|
|
400
|
+
|
|
401
|
+
Everything above assumes Stripe is the payment system. That is a reasonable
|
|
402
|
+
default and it is also a concentration: the account that runs the charges can be
|
|
403
|
+
limited or closed, and there is no second route unless one was built before it
|
|
404
|
+
was needed. Three things make adding a second provider cheap later and are
|
|
405
|
+
nearly free now — the entitlement keyed to **your** user id with provider ids as
|
|
406
|
+
fields, the customer identity resolved before checkout, and the reconciliation
|
|
407
|
+
job above, which is the only number in the system that is not your own telemetry.
|
|
408
|
+
|
|
409
|
+
At volume, three leaks that do not look like payment problems: approval rates
|
|
410
|
+
differing by country, renewals failing on reissued cards, and a `canceled`
|
|
411
|
+
status that cannot tell a decision from a bank reissue. Stripe's **automatic
|
|
412
|
+
card updates** cover some of the second one, with two limits worth knowing —
|
|
413
|
+
coverage varies by country and Stripe states you **cannot identify which cards
|
|
414
|
+
it covers**, so handle `payment_method.automatically_updated` and
|
|
415
|
+
`payment_method.updated` rather than assuming the save.
|
|
416
|
+
|
|
417
|
+
Full treatment, including the involuntary-versus-voluntary table:
|
|
418
|
+
[`references/provider-concentration.md`](references/provider-concentration.md).
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
398
422
|
## Local development
|
|
399
423
|
|
|
400
424
|
**Skip Stripe.** A `SKIP_BILLING=true` branch that creates an obviously fake
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Depending on one provider
|
|
2
|
+
|
|
3
|
+
**Load this when** revenue is growing, when a second market is being opened, when
|
|
4
|
+
involuntary churn needs separating from voluntary, or when someone asks what
|
|
5
|
+
happens if the payment account is limited.
|
|
6
|
+
|
|
7
|
+
Everything in `SKILL.md` assumes Stripe is the payment system. That is a
|
|
8
|
+
reasonable default and it is also a concentration: the account that runs the
|
|
9
|
+
charges can be limited or closed, and when it is, there is no second route unless
|
|
10
|
+
one was built before it was needed. The seam that makes a second route possible
|
|
11
|
+
costs almost nothing while there is one provider, and becomes a migration once
|
|
12
|
+
there are two.
|
|
13
|
+
|
|
14
|
+
## Contents
|
|
15
|
+
|
|
16
|
+
- [Keep the seam, not the second provider](#keep-the-seam-not-the-second-provider)
|
|
17
|
+
- [Three ways this costs money at volume](#three-ways-this-costs-money-at-volume)
|
|
18
|
+
- [Automatic card updates, and what they do not tell you](#automatic-card-updates-and-what-they-do-not-tell-you)
|
|
19
|
+
- [Involuntary is not voluntary](#involuntary-is-not-voluntary)
|
|
20
|
+
- [Checklist](#checklist)
|
|
21
|
+
|
|
22
|
+
## Keep the seam, not the second provider
|
|
23
|
+
|
|
24
|
+
Running two providers from day one is two webhook surfaces, two reconciliations,
|
|
25
|
+
two test matrices and two sets of tax obligations, for a problem most products
|
|
26
|
+
never have. Adding the second one later is cheap **if** three things were true
|
|
27
|
+
from the start, and expensive if they were not.
|
|
28
|
+
|
|
29
|
+
- **The entitlement is yours and the charge is theirs.** Your own row, keyed to
|
|
30
|
+
your own user id, with the provider's ids as fields on it rather than as its
|
|
31
|
+
primary key. `SKILL.md` → **Reconciliation** already assumes this: its loop
|
|
32
|
+
excludes "rows that were never Stripe's" — comped, manual and other-provider
|
|
33
|
+
plans with synthetic ids. That exclusion is this seam, half-built.
|
|
34
|
+
- **The customer identity is yours too.** Email or user id, resolved before
|
|
35
|
+
checkout, so a second provider can be added without re-identifying the base.
|
|
36
|
+
A provider's customer id as your only handle on a person is the migration
|
|
37
|
+
nobody budgets for.
|
|
38
|
+
- **The reconciliation job is the external check.** It is the one number in the
|
|
39
|
+
system that is not telemetry: charges that succeeded, reported by the party
|
|
40
|
+
that processed them. Everything else you know about revenue is inference from
|
|
41
|
+
your own instrumentation.
|
|
42
|
+
|
|
43
|
+
What this is **not**: an argument for an abstraction layer over payments. A
|
|
44
|
+
generic `PaymentProvider` interface written against one provider encodes that
|
|
45
|
+
provider's model and has to be rewritten for the second one anyway. The seam is
|
|
46
|
+
in the data — whose id owns what — not in the code shape.
|
|
47
|
+
|
|
48
|
+
## Three ways this costs money at volume
|
|
49
|
+
|
|
50
|
+
None of these looks like a payments problem from inside the app.
|
|
51
|
+
|
|
52
|
+
| What happens | Why it is invisible | Where to look |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Approval rates differ by country, and some markets are not served at all | An unserved market is obvious. A market with a poor approval rate is not: a declined charge and a customer who changed their mind produce the same empty result in the funnel | Decline codes grouped by country, measured against **attempts** rather than against successes |
|
|
55
|
+
| Renewals fail on cards that expired, were reissued, or were blocked | The subscription ends and nobody decided to end it, so it lands in churn beside voluntary cancellations | `invoice.payment_failed` and the dunning path, kept separate from the `customer.subscription.deleted` that came from your own UI |
|
|
56
|
+
| A replaced card silently keeps working, or silently does not | See the next section: the mechanism exists, its coverage varies by country, and Stripe states you cannot tell which cards it covers | `payment_method.automatically_updated` and `payment_method.updated` |
|
|
57
|
+
|
|
58
|
+
The measurement that makes the first row actionable is the denominator. Approval
|
|
59
|
+
rate is successes over *attempts*, and a dashboard that reports successes over
|
|
60
|
+
successes reports 100% in every market.
|
|
61
|
+
|
|
62
|
+
## Automatic card updates, and what they do not tell you
|
|
63
|
+
|
|
64
|
+
Stripe works with the card networks and **automatically** attempts to update
|
|
65
|
+
saved card details when a customer's card is replaced, so a saved payment method
|
|
66
|
+
can keep working after an expiry or a reissue. There is nothing to switch on.
|
|
67
|
+
|
|
68
|
+
Two limits, both stated by Stripe and both load-bearing:
|
|
69
|
+
|
|
70
|
+
- Support is **wide in the United States** across American Express, Visa,
|
|
71
|
+
Mastercard and Discover issued there, and **varies by country** elsewhere,
|
|
72
|
+
because it needs the issuer to participate.
|
|
73
|
+
- It is **not possible to identify which cards support it**. So you cannot
|
|
74
|
+
compute how much of your subscription base is protected, and any plan that
|
|
75
|
+
assumes the mechanism will catch a reissue is a plan with an unmeasurable
|
|
76
|
+
branch.
|
|
77
|
+
|
|
78
|
+
What to do rather than assume:
|
|
79
|
+
|
|
80
|
+
- Listen for **`payment_method.automatically_updated`** for network-driven
|
|
81
|
+
changes and **`payment_method.updated`** for API-driven ones.
|
|
82
|
+
- Both carry the card's new expiration and last four. Write them to your own
|
|
83
|
+
records from the event, or the billing page keeps showing digits the customer
|
|
84
|
+
no longer has, and a support ticket follows a change that actually succeeded.
|
|
85
|
+
- A card update that includes a new **number** changes the payment method's
|
|
86
|
+
`fingerprint`. If anything in your system keys off the fingerprint — dedup of
|
|
87
|
+
saved cards, fraud rules, "you already used this card" — that identity moves
|
|
88
|
+
under it.
|
|
89
|
+
|
|
90
|
+
## Involuntary is not voluntary
|
|
91
|
+
|
|
92
|
+
Stripe owns the retry schedule and the terminal state (`SKILL.md` →
|
|
93
|
+
**Renewal**, **Cancellation**). What it cannot own is the difference between a
|
|
94
|
+
subscription that ended because someone chose to leave and one that ended because
|
|
95
|
+
a bank reissued a number.
|
|
96
|
+
|
|
97
|
+
Keep them apart in your own data, because everything downstream reads it:
|
|
98
|
+
|
|
99
|
+
| | Voluntary | Involuntary |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| Arrives as | your own cancel route, or `cancel_at_period_end` | `invoice.payment_failed` → dunning → terminal |
|
|
102
|
+
| The customer's intent | stated | never expressed |
|
|
103
|
+
| The right message | confirm, and say what happens to their data | tell them a payment failed and how to fix it |
|
|
104
|
+
| The wrong message | — | a farewell email, which spends the relationship on a recoverable failure |
|
|
105
|
+
| Counts as churn | yes | yes, and reporting it in the same number hides a fixable revenue leak |
|
|
106
|
+
|
|
107
|
+
A single `canceled` status for both is the shape this failure takes. Record the
|
|
108
|
+
cause, not just the outcome.
|
|
109
|
+
|
|
110
|
+
## Checklist
|
|
111
|
+
|
|
112
|
+
- [ ] Entitlement rows keyed to your user id, provider ids as fields
|
|
113
|
+
- [ ] Customer identity (email or user id) resolved before checkout
|
|
114
|
+
- [ ] Reconciliation job running, and its result compared against your own numbers
|
|
115
|
+
- [ ] Decline codes grouped by country, measured against attempts
|
|
116
|
+
- [ ] `payment_method.automatically_updated` and `payment_method.updated` handled,
|
|
117
|
+
and the stored last-four and expiry written from them
|
|
118
|
+
- [ ] Anything keyed on `fingerprint` reviewed for the case where it changes
|
|
119
|
+
- [ ] Involuntary and voluntary cancellation distinguishable in your own data
|
|
120
|
+
- [ ] No farewell email on the involuntary path
|