Skip to content

Commit 810cf13

Browse files
RipperMercsclaude
andcommitted
feat(payments): /api/payment/history per-token credit-purchase audit log
Adds /api/payment/history (free with bearer token) returning the purchase audit trail for the requesting token: tx hash, amount, credits added, block number, confirmation timestamp. Pairs with the existing /api/payment/usage (spend side) so a bearer can reconcile its full token lifecycle. - worker/src/payments.ts: new pay:purchases:{token} ring buffer (cap 100), appendTokenPurchase() helper called on both confirm paths (explicit confirm + x402 retry), getPaymentHistory() reader. Backwards-compat: tokens minted before this ledger return current balance with empty purchases array. - worker/src/index.ts: route handler with the same Authorization: Bearer pattern as /api/payment/usage. - /api/meta and llms.txt updated. - src/lib/api-reference-directory.ts: full per-endpoint page entry at /api-reference/payment-history with FAQ, code samples, related links to payment-usage / payment-balance / payment-confirm. Unblocks TerminalFeed cross-site bundle, which needed this endpoint to surface deposit history alongside spend history. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ce7d828 commit 810cf13

5 files changed

Lines changed: 210 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ All mounted under `https://tensorfeed.ai/api/*` via the Worker.
204204
- `/api/payment/confirm` (POST): Verify USDC tx on-chain, mint a bearer token
205205
- `/api/payment/balance`: Read remaining credits for the current bearer token
206206
- `/api/payment/usage`: Per-token call history (last 100 calls aggregated by endpoint). Auth required, no credit cost. Powers the human /account dashboard.
207+
- `/api/payment/history`: Per-token credit-purchase audit log (which on-chain txs added how many credits and when). Auth required (`Authorization: Bearer <token>`), no credit cost. Pairs with `/api/payment/usage` (spend side) to give the bearer's full token lifecycle. Backed by `pay:purchases:{token}` ring buffer (cap 100); tokens minted before this ledger existed return an empty `purchases` array but still expose `current_balance` and `token_short`.
207208
- `/api/alerts/subscribe`: Outage alert email signup
208209
- `/api/refresh?key=production[&task=history]`: Manual data refresh / history capture trigger
209210

public/llms.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ TensorFeed accepts USDC on Base as the sole payment method for premium endpoints
6565
- [Confirm Payment](https://tensorfeed.ai/api/payment/confirm): POST { tx_hash, nonce? } to verify a USDC tx and mint a bearer token
6666
- [Check Balance](https://tensorfeed.ai/api/payment/balance): GET with `Authorization: Bearer tf_live_...` to see remaining credits
6767
- [Token Usage History](https://tensorfeed.ai/api/payment/usage): GET with bearer token to see per-endpoint call counts and the most recent calls (last 100 entries, no credit cost). Powers the human-facing /account dashboard.
68+
- [Token Payment History](https://tensorfeed.ai/api/payment/history): GET with bearer token to see the credit-purchase audit log (which on-chain txs added how many credits and when, last 100 purchases, no credit cost). Pairs with /api/payment/usage to cover the full token lifecycle (purchases + spend).
6869
- [Premium Routing](https://tensorfeed.ai/api/premium/routing): Tier 2, 1 credit per call. Top 5 ranked models with full score breakdown, pricing, status, component-level detail. Same query params as the preview plus optional ?top_n=1-10, ?w_quality=, ?w_availability=, ?w_cost=, ?w_latency= for custom weights.
6970
- [Premium Pricing Series](https://tensorfeed.ai/api/premium/history/pricing/series): Tier 1, 1 credit per call. Daily input/output/blended price points for one model across a date range, with min/max/delta summary. Params: `?model=<id-or-name>&from=YYYY-MM-DD&to=YYYY-MM-DD`. Range capped at 90 days, default 30 days back.
7071
- [Premium Benchmark Series](https://tensorfeed.ai/api/premium/history/benchmarks/series): Tier 1, 1 credit per call. Score evolution for a single benchmark (e.g. swe_bench, mmlu_pro, gpqa_diamond, math, human_eval) on one model. Params: `?model=&benchmark=&from=&to=`. Range capped at 90 days.

src/lib/api-reference-directory.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ const tf = new TensorFeed({ token: 'tf_live_...' });
576576
const usage = await tf.usage();
577577
console.log(\`\${usage.total_calls} calls, \${usage.total_credits_spent} credits\`);`,
578578
mcpTool: 'get_account_usage',
579-
relatedSlugs: ['payment-balance', 'payment-info'],
579+
relatedSlugs: ['payment-balance', 'payment-history', 'payment-info'],
580580
faqs: [
581581
{
582582
q: 'How far back does the usage history go?',
@@ -589,6 +589,69 @@ console.log(\`\${usage.total_calls} calls, \${usage.total_credits_spent} credits
589589
],
590590
},
591591

592+
// ── Free with token: payment history ─────────────────────────────
593+
{
594+
slug: 'payment-history',
595+
name: 'Token Payment History',
596+
path: '/api/payment/history',
597+
method: 'GET',
598+
tier: 'free-with-token',
599+
cost: 'Free with bearer token',
600+
category: 'payment',
601+
seoTitle: 'TensorFeed Payment History API: Per-Token Credit Purchase Audit Log',
602+
seoDescription:
603+
'TensorFeed /api/payment/history returns the bearer\'s credit-purchase audit log: which on-chain txs added how many credits and when. Free with bearer token.',
604+
intro:
605+
'The /api/payment/history endpoint returns your token\'s purchase audit trail: every USDC tx that funded credits on this bearer, with amount, credits added, block number, and confirmation timestamp. Pairs with /api/payment/usage (spend side) so an agent can reconcile its full token lifecycle.',
606+
whenToUse:
607+
'When an agent needs to audit how its credits were funded, reconcile on-chain spend against credit additions, or render a deposit history alongside spend history in an internal dashboard.',
608+
params: [],
609+
exampleResponse: `{
610+
"ok": true,
611+
"token_short": "tf_live_eb0d0155...",
612+
"current_balance": 49,
613+
"total_purchased_usd": 1.0,
614+
"total_credits_added": 50,
615+
"purchase_count": 1,
616+
"purchases": [
617+
{
618+
"tx_hash": "0xabc...",
619+
"amount_usd": 1.0,
620+
"credits_added": 50,
621+
"block_number": 12345678,
622+
"confirmed_at": "2026-04-27T17:49:32.918Z"
623+
}
624+
]
625+
}`,
626+
pythonExample: `from tensorfeed import TensorFeed
627+
628+
tf = TensorFeed(token="tf_live_...")
629+
history = tf.payment_history()
630+
for p in history["purchases"]:
631+
print(f"+{p['credits_added']} credits via {p['tx_hash'][:12]}...")`,
632+
typescriptExample: `import { TensorFeed } from 'tensorfeed';
633+
634+
const tf = new TensorFeed({ token: 'tf_live_...' });
635+
const history = await tf.paymentHistory();
636+
console.log(\`\${history.purchase_count} purchases totaling $\${history.total_purchased_usd}\`);`,
637+
mcpTool: null,
638+
relatedSlugs: ['payment-usage', 'payment-balance', 'payment-confirm'],
639+
faqs: [
640+
{
641+
q: 'How far back does the purchase history go?',
642+
a: 'The ring buffer holds the last 100 purchases per token. At one purchase per token in typical agent usage, that is effectively unbounded; only an agent that repeatedly tops up the same bearer rather than minting fresh tokens would ever roll off the buffer.',
643+
},
644+
{
645+
q: 'What about tokens minted before this ledger existed?',
646+
a: 'Older tokens still authenticate cleanly and return current_balance, but the purchases array will be empty until a new top-up is recorded. The replay-protection ledger at pay:tx:{txHash} is the authoritative cross-reference if you need to audit a pre-ledger purchase.',
647+
},
648+
{
649+
q: 'Does this cost a credit?',
650+
a: 'No. /api/payment/history is free for the holder of the bearer token, same as /api/payment/balance and /api/payment/usage.',
651+
},
652+
],
653+
},
654+
592655
// ── Premium: routing ─────────────────────────────────────────────
593656
{
594657
slug: 'premium-routing',

worker/src/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
getRollup,
4848
listRollupDates,
4949
getTokenUsage,
50+
getPaymentHistory,
5051
validateAndCharge,
5152
} from './payments';
5253
import { recordPollRun, checkNewsStaleness, alertStaleNews, sendDailySummary, getAlertsStatus } from './alerts';
@@ -523,6 +524,7 @@ export default {
523524
paymentConfirm: '/api/payment/confirm',
524525
paymentBalance: '/api/payment/balance',
525526
paymentUsage: '/api/payment/usage',
527+
paymentHistory: '/api/payment/history',
526528
},
527529
admin: {
528530
usage: '/api/admin/usage?date=YYYY-MM-DD&key=<env>',
@@ -721,6 +723,29 @@ export default {
721723
return jsonResponse(result, 200, 0);
722724
}
723725

726+
// Per-token payment history. Audit log of credit purchases scoped
727+
// to the requesting bearer: which on-chain txs added how many
728+
// credits and when. Free, no credit cost. Backwards-compatible
729+
// with tokens minted before this ledger existed (returns empty
730+
// purchases array). Paired with /api/payment/usage which logs the
731+
// spend side; together they cover the full token lifecycle.
732+
733+
if (path === '/api/payment/history') {
734+
const authHeader = request.headers.get('Authorization');
735+
const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7).trim() : '';
736+
if (!token) {
737+
return jsonResponse(
738+
{ ok: false, error: 'token_required', message: 'Send the bearer token via Authorization: Bearer <token>' },
739+
401,
740+
);
741+
}
742+
const result = await getPaymentHistory(env, token);
743+
if (!result) {
744+
return jsonResponse({ ok: false, error: 'token_not_found' }, 404);
745+
}
746+
return jsonResponse(result, 200, 0);
747+
}
748+
724749
// === PAID PREMIUM ENDPOINT: ROUTING (Tier 2, requires credits) ===
725750

726751
if (path === '/api/premium/routing') {

worker/src/payments.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,111 @@ export interface TokenUsageSummary {
387387
recent: TokenUsageEntry[];
388388
}
389389

390+
// ── Per-token payment history (purchases) ───────────────────────────
391+
//
392+
// pay:purchases:{token} holds an append-only list of credit purchases
393+
// scoped to one bearer token. Used by /api/payment/history so an
394+
// agent can audit how its credits were funded (which on-chain txs
395+
// added how many credits and when), independent of /api/payment/usage
396+
// which logs how those credits were *spent*.
397+
//
398+
// Backward compat: tokens that confirmed before this field was added
399+
// will read an empty list. Existing pay:tx:{txHash} records remain the
400+
// authoritative replay-protection ledger; this is purely a per-token
401+
// view layered on top.
402+
403+
interface PurchaseEntry {
404+
tx_hash: string;
405+
amount_usd: number;
406+
credits_added: number;
407+
block_number?: number;
408+
confirmed_at: string;
409+
}
410+
411+
interface PurchaseRecord {
412+
entries: PurchaseEntry[];
413+
}
414+
415+
const PURCHASES_CAP = 100;
416+
417+
export async function appendTokenPurchase(
418+
env: Env,
419+
token: string,
420+
purchase: PurchaseEntry,
421+
): Promise<void> {
422+
try {
423+
const existing = (await env.TENSORFEED_CACHE.get(
424+
`pay:purchases:${token}`,
425+
'json',
426+
)) as PurchaseRecord | null;
427+
const entries = existing?.entries ?? [];
428+
entries.push(purchase);
429+
if (entries.length > PURCHASES_CAP) {
430+
entries.splice(0, entries.length - PURCHASES_CAP);
431+
}
432+
await env.TENSORFEED_CACHE.put(
433+
`pay:purchases:${token}`,
434+
JSON.stringify({ entries }),
435+
);
436+
} catch (e) {
437+
console.error('appendTokenPurchase failed:', e);
438+
}
439+
}
440+
441+
export interface PaymentHistorySummary {
442+
ok: true;
443+
token_short: string;
444+
current_balance: number;
445+
total_purchased_usd: number;
446+
total_credits_added: number;
447+
purchase_count: number;
448+
purchases: PurchaseEntry[];
449+
}
450+
451+
/**
452+
* Read the per-token purchase ledger. Auth-bound: caller must already
453+
* have validated the bearer token. Returns null if the token is not
454+
* recognized so the route handler can return 404 without leaking
455+
* existence to a wrong-secret guess.
456+
*/
457+
export async function getPaymentHistory(
458+
env: Env,
459+
token: string,
460+
): Promise<PaymentHistorySummary | null> {
461+
if (!token.startsWith('tf_live_')) return null;
462+
const credits = (await env.TENSORFEED_CACHE.get(
463+
`pay:credits:${token}`,
464+
'json',
465+
)) as CreditsRecord | null;
466+
if (!credits) return null;
467+
468+
const ledger = (await env.TENSORFEED_CACHE.get(
469+
`pay:purchases:${token}`,
470+
'json',
471+
)) as PurchaseRecord | null;
472+
const entries = ledger?.entries ?? [];
473+
474+
let totalUsd = 0;
475+
let totalCredits = 0;
476+
for (const e of entries) {
477+
totalUsd += e.amount_usd;
478+
totalCredits += e.credits_added;
479+
}
480+
481+
// Newest first for caller convenience.
482+
const sorted = entries.slice().sort((a, b) => (a.confirmed_at < b.confirmed_at ? 1 : -1));
483+
484+
return {
485+
ok: true,
486+
token_short: token.slice(0, 16) + '...',
487+
current_balance: credits.balance,
488+
total_purchased_usd: Number(totalUsd.toFixed(6)),
489+
total_credits_added: totalCredits,
490+
purchase_count: entries.length,
491+
purchases: sorted,
492+
};
493+
}
494+
390495
/**
391496
* Read the per-token usage ring buffer and aggregate it for the dashboard.
392497
* Returns null if the token isn't recognized.
@@ -601,6 +706,13 @@ export async function confirmPayment(
601706
env.TENSORFEED_CACHE.put(`pay:tx:${txHash}`, JSON.stringify(txRec)),
602707
nonce ? env.TENSORFEED_CACHE.delete(`pay:quote:${nonce}`) : Promise.resolve(),
603708
logRevenue(env, verified.amountUsd, request.headers.get('User-Agent') || 'unknown'),
709+
appendTokenPurchase(env, token, {
710+
tx_hash: txHash,
711+
amount_usd: verified.amountUsd,
712+
credits_added: credits,
713+
block_number: verified.blockNumber,
714+
confirmed_at: now,
715+
}),
604716
]);
605717

606718
return {
@@ -836,6 +948,13 @@ export async function requirePayment(
836948
env.TENSORFEED_CACHE.put(`pay:credits:${token}`, JSON.stringify(tokenRecord)),
837949
env.TENSORFEED_CACHE.put(`pay:tx:${txHash}`, JSON.stringify(txRec)),
838950
logRevenue(env, verified.amountUsd, request.headers.get('User-Agent') || 'unknown'),
951+
appendTokenPurchase(env, token, {
952+
tx_hash: txHash,
953+
amount_usd: verified.amountUsd,
954+
credits_added: credits,
955+
block_number: verified.blockNumber,
956+
confirmed_at: now,
957+
}),
839958
]);
840959

841960
return {

0 commit comments

Comments
 (0)