@agentbadge/payment-forensics 0.0.0-stage → 0.1.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/README.md +48 -2
- package/dist/funding.d.ts +21 -0
- package/dist/funding.js +61 -0
- package/dist/graph.d.ts +10 -0
- package/dist/graph.js +23 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/loops.d.ts +4 -0
- package/dist/loops.js +85 -0
- package/dist/score.d.ts +10 -0
- package/dist/score.js +108 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.js +1 -0
- package/package.json +35 -4
package/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @agentbadge/payment-forensics
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pure-TypeScript payment-graph forensics core for x402 settles on Arc. **Zero I/O** — no fetch/fs/db; callers supply edges and funding events.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Distinguishes **organic revenue** from **circular/farmed volume** per seller address:
|
|
8
|
+
|
|
9
|
+
- **Loop detection** — Tarjan SCC over the payment graph. Inside a loop only *net inflow* counts: money the seller pumped back into its own loop is netted out.
|
|
10
|
+
- **Self-funding strike** — payer whose *first* funder is the seller (or a wallet the seller funded, one hop) → zero organic.
|
|
11
|
+
- **Shared-funder strike** — ≥2 of the seller's payers sharing one *private* first funder → all struck. Public sources (exchange, bridge, `custody_provider`, contract) are exempt — a custodian like Crossmint funding thousands of agents is not a farm.
|
|
12
|
+
- **effectivePayers** — inverse Herfindahl over organic volumes (one dominant payer → ≈1).
|
|
13
|
+
- **coveredPct** — honest coverage: share of payers with observed funding data.
|
|
14
|
+
- **thinFile** — D-204-5 flag: <5 settles or <2 payers or <$0.05 gross → callers should answer `no_data`, not a verdict.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { analyzeAddress } from "@agentbadge/payment-forensics";
|
|
20
|
+
|
|
21
|
+
const analysis = analyzeAddress(seller, edges, fundingEvents, {
|
|
22
|
+
publicSources: [
|
|
23
|
+
{ address: "0x…", kind: "custody_provider", name: "Crossmint" },
|
|
24
|
+
{ address: "0x…", kind: "exchange" },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
// → { organicShare, organicVolume, grossVolume, effectivePayers,
|
|
28
|
+
// loops, selfFunded, sharedFunder, coveredPct, thinFile, payers }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Conventions
|
|
32
|
+
|
|
33
|
+
- Addresses normalized to lowercase internally.
|
|
34
|
+
- Amounts: `bigint`, USDC atomic units (decimals=6).
|
|
35
|
+
- `PaymentEdge` = one settle `{payer, seller, amount, txHash, block, ts}` — built by the 204-4 indexer from `AuthorizationUsed`/`Transfer` logs.
|
|
36
|
+
- `FundingEvent` = `{funder, funded, ts}` — first observed funding transfer.
|
|
37
|
+
|
|
38
|
+
## Scripts
|
|
39
|
+
|
|
40
|
+
- `bun run build` / `typecheck` — tsc → dist
|
|
41
|
+
- `bun test` / `bun run test` — vitest (5 forensic scenarios)
|
|
42
|
+
|
|
43
|
+
Reference: Assay `packages/core` (MIT) model, adapted to Arc USDC per EPIC-204.
|
|
44
|
+
|
|
45
|
+
## Questions & Contact
|
|
46
|
+
|
|
47
|
+
- **Site**: https://agentbadge.xyz/
|
|
48
|
+
- **Contact page**: https://agentbadge.xyz/contact
|
|
49
|
+
- **Discord / Telegram / e-mail**: see https://agentbadge.xyz/contact
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Address, FundingEvent, PublicSource, PublicSourceKind } from "./types.js";
|
|
2
|
+
export type PublicSourceMap = Map<Address, PublicSourceKind>;
|
|
3
|
+
export declare function toPublicSourceMap(sources: PublicSource[] | PublicSourceMap | undefined): PublicSourceMap;
|
|
4
|
+
export declare function firstFunders(funding: FundingEvent[]): Map<Address, Address>;
|
|
5
|
+
export type FundingVerdict = {
|
|
6
|
+
kind: "organic";
|
|
7
|
+
} | {
|
|
8
|
+
kind: "unknown";
|
|
9
|
+
} | {
|
|
10
|
+
kind: "public";
|
|
11
|
+
funder: Address;
|
|
12
|
+
sourceKind: PublicSourceKind;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "self_funded";
|
|
15
|
+
funder: Address;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "shared_funder";
|
|
18
|
+
funder: Address;
|
|
19
|
+
};
|
|
20
|
+
export declare function classifyFunding(payer: Address, seller: Address, first: Map<Address, Address>, publicSources: PublicSourceMap, funderPayerCounts: Map<Address, number>, sharedMin?: number): FundingVerdict;
|
|
21
|
+
export declare function funderPayerCounts(payers: Address[], first: Map<Address, Address>, publicSources: PublicSourceMap): Map<Address, number>;
|
package/dist/funding.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { norm } from "./graph.js";
|
|
2
|
+
export function toPublicSourceMap(sources) {
|
|
3
|
+
if (!sources)
|
|
4
|
+
return new Map();
|
|
5
|
+
if (sources instanceof Map) {
|
|
6
|
+
const m = new Map();
|
|
7
|
+
for (const [a, k] of sources)
|
|
8
|
+
m.set(norm(a), k);
|
|
9
|
+
return m;
|
|
10
|
+
}
|
|
11
|
+
return new Map(sources.map((s) => [norm(s.address), s.kind]));
|
|
12
|
+
}
|
|
13
|
+
export function firstFunders(funding) {
|
|
14
|
+
const first = new Map();
|
|
15
|
+
for (const f of funding) {
|
|
16
|
+
const funded = norm(f.funded);
|
|
17
|
+
const cur = first.get(funded);
|
|
18
|
+
if (!cur || f.ts < cur.ts) {
|
|
19
|
+
first.set(funded, { funder: norm(f.funder), ts: f.ts });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const out = new Map();
|
|
23
|
+
for (const [a, v] of first)
|
|
24
|
+
out.set(a, v.funder);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
export function classifyFunding(payer, seller, first, publicSources, funderPayerCounts, sharedMin = 2) {
|
|
28
|
+
const p = norm(payer);
|
|
29
|
+
const s = norm(seller);
|
|
30
|
+
const funder = first.get(p);
|
|
31
|
+
if (!funder)
|
|
32
|
+
return { kind: "unknown" };
|
|
33
|
+
if (funder === s)
|
|
34
|
+
return { kind: "self_funded", funder };
|
|
35
|
+
const kind = publicSources.get(funder);
|
|
36
|
+
if (kind)
|
|
37
|
+
return { kind: "public", funder, sourceKind: kind };
|
|
38
|
+
if (first.get(funder) === s)
|
|
39
|
+
return { kind: "self_funded", funder };
|
|
40
|
+
if ((funderPayerCounts.get(funder) ?? 0) >= sharedMin) {
|
|
41
|
+
return { kind: "shared_funder", funder };
|
|
42
|
+
}
|
|
43
|
+
return { kind: "organic" };
|
|
44
|
+
}
|
|
45
|
+
export function funderPayerCounts(payers, first, publicSources) {
|
|
46
|
+
const counts = new Map();
|
|
47
|
+
for (const raw of payers) {
|
|
48
|
+
const p = norm(raw);
|
|
49
|
+
const f = first.get(p);
|
|
50
|
+
if (!f || publicSources.has(f))
|
|
51
|
+
continue;
|
|
52
|
+
let set = counts.get(f);
|
|
53
|
+
if (!set)
|
|
54
|
+
counts.set(f, (set = new Set()));
|
|
55
|
+
set.add(p);
|
|
56
|
+
}
|
|
57
|
+
const out = new Map();
|
|
58
|
+
for (const [f, set] of counts)
|
|
59
|
+
out.set(f, set.size);
|
|
60
|
+
return out;
|
|
61
|
+
}
|
package/dist/graph.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Address, PaymentEdge } from "./types.js";
|
|
2
|
+
export declare const norm: (a: Address) => Address;
|
|
3
|
+
export interface PaymentGraph {
|
|
4
|
+
nodes: Set<Address>;
|
|
5
|
+
adjacency: Map<Address, Map<Address, bigint>>;
|
|
6
|
+
outVolume: Map<Address, bigint>;
|
|
7
|
+
inVolume: Map<Address, bigint>;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildGraph(edges: PaymentEdge[]): PaymentGraph;
|
|
10
|
+
export declare function edgeVolume(g: PaymentGraph, from: Address, to: Address): bigint;
|
package/dist/graph.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const norm = (a) => a.toLowerCase();
|
|
2
|
+
export function buildGraph(edges) {
|
|
3
|
+
const nodes = new Set();
|
|
4
|
+
const adjacency = new Map();
|
|
5
|
+
const outVolume = new Map();
|
|
6
|
+
const inVolume = new Map();
|
|
7
|
+
for (const e of edges) {
|
|
8
|
+
const from = norm(e.payer);
|
|
9
|
+
const to = norm(e.seller);
|
|
10
|
+
nodes.add(from);
|
|
11
|
+
nodes.add(to);
|
|
12
|
+
let row = adjacency.get(from);
|
|
13
|
+
if (!row)
|
|
14
|
+
adjacency.set(from, (row = new Map()));
|
|
15
|
+
row.set(to, (row.get(to) ?? 0n) + e.amount);
|
|
16
|
+
outVolume.set(from, (outVolume.get(from) ?? 0n) + e.amount);
|
|
17
|
+
inVolume.set(to, (inVolume.get(to) ?? 0n) + e.amount);
|
|
18
|
+
}
|
|
19
|
+
return { nodes, adjacency, outVolume, inVolume };
|
|
20
|
+
}
|
|
21
|
+
export function edgeVolume(g, from, to) {
|
|
22
|
+
return g.adjacency.get(norm(from))?.get(norm(to)) ?? 0n;
|
|
23
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export { buildGraph, edgeVolume, norm, type PaymentGraph } from "./graph.js";
|
|
3
|
+
export { findSccs, loopsFor } from "./loops.js";
|
|
4
|
+
export { classifyFunding, firstFunders, funderPayerCounts, toPublicSourceMap, type FundingVerdict, type PublicSourceMap, } from "./funding.js";
|
|
5
|
+
export { analyzeAddress, THIN_GROSS_FLOOR, THIN_MIN_PAYERS, THIN_MIN_SETTLES, type AnalyzeOptions, } from "./score.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export { buildGraph, edgeVolume, norm } from "./graph.js";
|
|
3
|
+
export { findSccs, loopsFor } from "./loops.js";
|
|
4
|
+
export { classifyFunding, firstFunders, funderPayerCounts, toPublicSourceMap, } from "./funding.js";
|
|
5
|
+
export { analyzeAddress, THIN_GROSS_FLOOR, THIN_MIN_PAYERS, THIN_MIN_SETTLES, } from "./score.js";
|
package/dist/loops.d.ts
ADDED
package/dist/loops.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { buildGraph, edgeVolume, norm } from "./graph.js";
|
|
2
|
+
export function findSccs(g) {
|
|
3
|
+
const index = new Map();
|
|
4
|
+
const low = new Map();
|
|
5
|
+
const onStack = new Set();
|
|
6
|
+
const stack = [];
|
|
7
|
+
const sccs = [];
|
|
8
|
+
let counter = 0;
|
|
9
|
+
for (const start of g.nodes) {
|
|
10
|
+
if (index.has(start))
|
|
11
|
+
continue;
|
|
12
|
+
const work = [
|
|
13
|
+
[start, (g.adjacency.get(start) ?? new Map()).keys()],
|
|
14
|
+
];
|
|
15
|
+
index.set(start, counter);
|
|
16
|
+
low.set(start, counter);
|
|
17
|
+
counter++;
|
|
18
|
+
stack.push(start);
|
|
19
|
+
onStack.add(start);
|
|
20
|
+
while (work.length > 0) {
|
|
21
|
+
const [node, it] = work[work.length - 1];
|
|
22
|
+
const next = it.next();
|
|
23
|
+
if (next.done) {
|
|
24
|
+
if (low.get(node) === index.get(node)) {
|
|
25
|
+
const scc = [];
|
|
26
|
+
for (;;) {
|
|
27
|
+
const w = stack.pop();
|
|
28
|
+
onStack.delete(w);
|
|
29
|
+
scc.push(w);
|
|
30
|
+
if (w === node)
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if (scc.length > 1 || edgeVolume(g, node, node) > 0n)
|
|
34
|
+
sccs.push(scc);
|
|
35
|
+
}
|
|
36
|
+
work.pop();
|
|
37
|
+
if (work.length > 0) {
|
|
38
|
+
const parent = work[work.length - 1][0];
|
|
39
|
+
low.set(parent, Math.min(low.get(parent), low.get(node)));
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const w = next.value;
|
|
44
|
+
if (!index.has(w)) {
|
|
45
|
+
index.set(w, counter);
|
|
46
|
+
low.set(w, counter);
|
|
47
|
+
counter++;
|
|
48
|
+
stack.push(w);
|
|
49
|
+
onStack.add(w);
|
|
50
|
+
work.push([w, (g.adjacency.get(w) ?? new Map()).keys()]);
|
|
51
|
+
}
|
|
52
|
+
else if (onStack.has(w)) {
|
|
53
|
+
low.set(node, Math.min(low.get(node), index.get(w)));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return sccs;
|
|
58
|
+
}
|
|
59
|
+
export function loopsFor(seller, edges) {
|
|
60
|
+
const s = norm(seller);
|
|
61
|
+
const g = buildGraph(edges);
|
|
62
|
+
const sccs = findSccs(g);
|
|
63
|
+
const loops = [];
|
|
64
|
+
for (const members of sccs) {
|
|
65
|
+
if (!members.includes(s))
|
|
66
|
+
continue;
|
|
67
|
+
const set = new Set(members);
|
|
68
|
+
let grossInternal = 0n;
|
|
69
|
+
let inflow = 0n;
|
|
70
|
+
let outflow = 0n;
|
|
71
|
+
for (const e of edges) {
|
|
72
|
+
const p = norm(e.payer);
|
|
73
|
+
const t = norm(e.seller);
|
|
74
|
+
if (set.has(p) && set.has(t))
|
|
75
|
+
grossInternal += e.amount;
|
|
76
|
+
if (set.has(p) && t === s)
|
|
77
|
+
inflow += e.amount;
|
|
78
|
+
if (p === s && set.has(t) && t !== s)
|
|
79
|
+
outflow += e.amount;
|
|
80
|
+
}
|
|
81
|
+
const netInflow = inflow - outflow > 0n ? inflow - outflow : 0n;
|
|
82
|
+
loops.push({ members: members.sort(), grossInternal, netInflow });
|
|
83
|
+
}
|
|
84
|
+
return loops;
|
|
85
|
+
}
|
package/dist/score.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Address, AddressAnalysis, FundingEvent, PaymentEdge, PublicSource } from "./types.js";
|
|
2
|
+
import { type PublicSourceMap } from "./funding.js";
|
|
3
|
+
export interface AnalyzeOptions {
|
|
4
|
+
publicSources?: PublicSource[] | PublicSourceMap;
|
|
5
|
+
sharedFunderMin?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const THIN_GROSS_FLOOR = 50000n;
|
|
8
|
+
export declare const THIN_MIN_SETTLES = 5;
|
|
9
|
+
export declare const THIN_MIN_PAYERS = 2;
|
|
10
|
+
export declare function analyzeAddress(address: Address, edges: PaymentEdge[], funding: FundingEvent[], opts?: AnalyzeOptions): AddressAnalysis;
|
package/dist/score.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { norm } from "./graph.js";
|
|
2
|
+
import { loopsFor } from "./loops.js";
|
|
3
|
+
import { classifyFunding, funderPayerCounts, firstFunders, toPublicSourceMap, } from "./funding.js";
|
|
4
|
+
export const THIN_GROSS_FLOOR = 50000n;
|
|
5
|
+
export const THIN_MIN_SETTLES = 5;
|
|
6
|
+
export const THIN_MIN_PAYERS = 2;
|
|
7
|
+
export function analyzeAddress(address, edges, funding, opts = {}) {
|
|
8
|
+
const seller = norm(address);
|
|
9
|
+
const pub = toPublicSourceMap(opts.publicSources);
|
|
10
|
+
const sharedMin = opts.sharedFunderMin ?? 2;
|
|
11
|
+
const inbound = edges.filter((e) => norm(e.seller) === seller);
|
|
12
|
+
const gross = inbound.reduce((a, e) => a + e.amount, 0n);
|
|
13
|
+
const payerVolumes = new Map();
|
|
14
|
+
for (const e of inbound) {
|
|
15
|
+
const p = norm(e.payer);
|
|
16
|
+
payerVolumes.set(p, (payerVolumes.get(p) ?? 0n) + e.amount);
|
|
17
|
+
}
|
|
18
|
+
const payerList = [...payerVolumes.keys()];
|
|
19
|
+
const loops = loopsFor(seller, edges);
|
|
20
|
+
const loopOf = new Map();
|
|
21
|
+
for (const l of loops)
|
|
22
|
+
for (const m of l.members)
|
|
23
|
+
if (m !== seller)
|
|
24
|
+
loopOf.set(m, l);
|
|
25
|
+
const first = firstFunders(funding);
|
|
26
|
+
const counts = funderPayerCounts(payerList, first, pub);
|
|
27
|
+
const selfFunded = [];
|
|
28
|
+
const sharedFunder = [];
|
|
29
|
+
const payers = [];
|
|
30
|
+
let covered = 0;
|
|
31
|
+
let organic = 0n;
|
|
32
|
+
for (const p of payerList) {
|
|
33
|
+
const v = payerVolumes.get(p);
|
|
34
|
+
const vrd = classifyFunding(p, seller, first, pub, counts, sharedMin);
|
|
35
|
+
if (vrd.kind !== "unknown")
|
|
36
|
+
covered++;
|
|
37
|
+
let loopOrganic = v;
|
|
38
|
+
const loop = loopOf.get(p);
|
|
39
|
+
let inLoop = false;
|
|
40
|
+
if (loop) {
|
|
41
|
+
inLoop = true;
|
|
42
|
+
const memberIn = loop.members
|
|
43
|
+
.filter((m) => m !== seller)
|
|
44
|
+
.reduce((a, m) => a + (payerVolumes.get(m) ?? 0n), 0n);
|
|
45
|
+
loopOrganic = memberIn > 0n ? (v * loop.netInflow) / memberIn : 0n;
|
|
46
|
+
}
|
|
47
|
+
let organicV;
|
|
48
|
+
let struck = false;
|
|
49
|
+
let strikeReason;
|
|
50
|
+
let funder;
|
|
51
|
+
if (vrd.kind === "self_funded") {
|
|
52
|
+
organicV = 0n;
|
|
53
|
+
struck = true;
|
|
54
|
+
strikeReason = "self_funded";
|
|
55
|
+
funder = vrd.funder;
|
|
56
|
+
selfFunded.push(p);
|
|
57
|
+
}
|
|
58
|
+
else if (vrd.kind === "shared_funder") {
|
|
59
|
+
organicV = 0n;
|
|
60
|
+
struck = true;
|
|
61
|
+
strikeReason = "shared_funder";
|
|
62
|
+
funder = vrd.funder;
|
|
63
|
+
sharedFunder.push(p);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
organicV = loopOrganic;
|
|
67
|
+
struck = inLoop && organicV === 0n;
|
|
68
|
+
if (inLoop)
|
|
69
|
+
strikeReason = "loop";
|
|
70
|
+
}
|
|
71
|
+
organic += organicV;
|
|
72
|
+
payers.push({
|
|
73
|
+
payer: p,
|
|
74
|
+
grossVolume: v,
|
|
75
|
+
organicVolume: organicV,
|
|
76
|
+
struck,
|
|
77
|
+
strikeReason,
|
|
78
|
+
funder,
|
|
79
|
+
fundingUnknown: vrd.kind === "unknown",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const organicShare = gross > 0n ? Number(organic) / Number(gross) : 0;
|
|
83
|
+
let hhi = 0;
|
|
84
|
+
if (organic > 0n) {
|
|
85
|
+
for (const p of payers) {
|
|
86
|
+
const s = Number(p.organicVolume) / Number(organic);
|
|
87
|
+
hhi += s * s;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const effectivePayers = hhi > 0 ? 1 / hhi : 0;
|
|
91
|
+
const thinFile = inbound.length < THIN_MIN_SETTLES ||
|
|
92
|
+
payerList.length < THIN_MIN_PAYERS ||
|
|
93
|
+
gross < THIN_GROSS_FLOOR;
|
|
94
|
+
return {
|
|
95
|
+
address: seller,
|
|
96
|
+
payerCount: payerList.length,
|
|
97
|
+
grossVolume: gross,
|
|
98
|
+
organicVolume: organic,
|
|
99
|
+
organicShare,
|
|
100
|
+
effectivePayers,
|
|
101
|
+
loops,
|
|
102
|
+
selfFunded,
|
|
103
|
+
sharedFunder,
|
|
104
|
+
coveredPct: payerList.length > 0 ? covered / payerList.length : 0,
|
|
105
|
+
thinFile,
|
|
106
|
+
payers,
|
|
107
|
+
};
|
|
108
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type Address = string;
|
|
2
|
+
export interface PaymentEdge {
|
|
3
|
+
payer: Address;
|
|
4
|
+
seller: Address;
|
|
5
|
+
amount: bigint;
|
|
6
|
+
txHash: string;
|
|
7
|
+
block: number;
|
|
8
|
+
ts: number;
|
|
9
|
+
}
|
|
10
|
+
export interface FundingEvent {
|
|
11
|
+
funder: Address;
|
|
12
|
+
funded: Address;
|
|
13
|
+
ts: number;
|
|
14
|
+
txHash?: string;
|
|
15
|
+
}
|
|
16
|
+
export type PublicSourceKind = "exchange" | "bridge" | "custody_provider" | "contract";
|
|
17
|
+
export interface PublicSource {
|
|
18
|
+
address: Address;
|
|
19
|
+
kind: PublicSourceKind;
|
|
20
|
+
name?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface LoopInfo {
|
|
23
|
+
members: Address[];
|
|
24
|
+
grossInternal: bigint;
|
|
25
|
+
netInflow: bigint;
|
|
26
|
+
}
|
|
27
|
+
export interface PayerBreakdown {
|
|
28
|
+
payer: Address;
|
|
29
|
+
grossVolume: bigint;
|
|
30
|
+
organicVolume: bigint;
|
|
31
|
+
struck: boolean;
|
|
32
|
+
strikeReason?: "loop" | "self_funded" | "shared_funder";
|
|
33
|
+
funder?: Address;
|
|
34
|
+
fundingUnknown: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface AddressAnalysis {
|
|
37
|
+
address: Address;
|
|
38
|
+
payerCount: number;
|
|
39
|
+
grossVolume: bigint;
|
|
40
|
+
organicVolume: bigint;
|
|
41
|
+
organicShare: number;
|
|
42
|
+
effectivePayers: number;
|
|
43
|
+
loops: LoopInfo[];
|
|
44
|
+
selfFunded: Address[];
|
|
45
|
+
sharedFunder: Address[];
|
|
46
|
+
coveredPct: number;
|
|
47
|
+
thinFile: boolean;
|
|
48
|
+
payers: PayerBreakdown[];
|
|
49
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentbadge/payment-forensics",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Pure-TypeScript payment-graph forensics core — loop detection (Tarjan SCC), self-funding trace, shared-funder strike, organic revenue share. Zero I/O.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"author": "AgentBadge",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"x402",
|
|
15
|
+
"arc",
|
|
16
|
+
"usdc",
|
|
17
|
+
"payment-forensics",
|
|
18
|
+
"organic-revenue",
|
|
19
|
+
"agentbadge"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"test": "vitest run"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/bun": "^1.2.0",
|
|
34
|
+
"typescript": "^5.6.0",
|
|
35
|
+
"vitest": "^3.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|