Release 202505310921
This commit is contained in:
5
web/src/actions/api/index.ts
Normal file
5
web/src/actions/api/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { apiServiceActions } from './service'
|
||||||
|
|
||||||
|
export const apiActions = {
|
||||||
|
service: apiServiceActions,
|
||||||
|
}
|
||||||
113
web/src/actions/api/service.ts
Normal file
113
web/src/actions/api/service.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { z } from 'astro/zod'
|
||||||
|
import { ActionError } from 'astro:actions'
|
||||||
|
import { pick } from 'lodash-es'
|
||||||
|
|
||||||
|
import { getKycLevelInfo } from '../../constants/kycLevels'
|
||||||
|
import { getVerificationStatusInfo } from '../../constants/verificationStatus'
|
||||||
|
import { defineProtectedAction } from '../../lib/defineProtectedAction'
|
||||||
|
import { prisma } from '../../lib/prisma'
|
||||||
|
import { zodUrlOptionalProtocol } from '../../lib/zodUtils'
|
||||||
|
|
||||||
|
import type { Prisma } from '@prisma/client'
|
||||||
|
|
||||||
|
export const apiServiceActions = {
|
||||||
|
get: defineProtectedAction({
|
||||||
|
accept: 'json',
|
||||||
|
permissions: 'guest',
|
||||||
|
input: z.object({
|
||||||
|
id: z.coerce.number().int().positive().optional(),
|
||||||
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(2048)
|
||||||
|
.regex(/^[a-z0-9-]+$/, 'Allowed characters: lowercase letters, numbers, and hyphens')
|
||||||
|
.optional(),
|
||||||
|
url: zodUrlOptionalProtocol.optional(),
|
||||||
|
}),
|
||||||
|
handler: async (input) => {
|
||||||
|
if (!input.id && !input.slug && !input.url) {
|
||||||
|
throw new ActionError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'At least one of the following parameters is required: id, slug, url',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlVariants = input.url
|
||||||
|
? [input.url]
|
||||||
|
.flatMap((url) =>
|
||||||
|
[
|
||||||
|
url,
|
||||||
|
url.startsWith('http://') ? url.replace('http://', 'https://') : undefined,
|
||||||
|
url.startsWith('https://') ? url.replace('https://', 'http://') : undefined,
|
||||||
|
].filter((url) => url !== undefined)
|
||||||
|
)
|
||||||
|
.flatMap((url) => [url, url.endsWith('/') ? url.slice(0, -1) : `${url}/`])
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const service = await prisma.service.findFirst({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
...(input.id ? ([{ id: input.id }] satisfies Prisma.ServiceWhereInput[]) : []),
|
||||||
|
...(input.slug ? ([{ slug: input.slug }] satisfies Prisma.ServiceWhereInput[]) : []),
|
||||||
|
...(urlVariants
|
||||||
|
? ([
|
||||||
|
{ serviceUrls: { hasSome: urlVariants } },
|
||||||
|
{ onionUrls: { hasSome: urlVariants } },
|
||||||
|
{ i2pUrls: { hasSome: urlVariants } },
|
||||||
|
] satisfies Prisma.ServiceWhereInput[])
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
description: true,
|
||||||
|
kycLevel: true,
|
||||||
|
verificationStatus: true,
|
||||||
|
categories: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceUrls: true,
|
||||||
|
onionUrls: true,
|
||||||
|
i2pUrls: true,
|
||||||
|
tosUrls: true,
|
||||||
|
referral: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
throw new ActionError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'Service not found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: service.id,
|
||||||
|
slug: service.slug,
|
||||||
|
name: service.name,
|
||||||
|
description: service.description,
|
||||||
|
verificationStatus: service.verificationStatus,
|
||||||
|
verificationStatusInfo: pick(getVerificationStatusInfo(service.verificationStatus), [
|
||||||
|
'value',
|
||||||
|
'slug',
|
||||||
|
'label',
|
||||||
|
'labelShort',
|
||||||
|
'description',
|
||||||
|
]),
|
||||||
|
kycLevel: service.kycLevel,
|
||||||
|
kycLevelInfo: pick(getKycLevelInfo(service.kycLevel.toString()), ['value', 'name', 'description']),
|
||||||
|
categories: service.categories,
|
||||||
|
serviceUrls: [...service.serviceUrls, ...service.onionUrls, ...service.i2pUrls].map(
|
||||||
|
(url) => url + (service.referral ?? '')
|
||||||
|
),
|
||||||
|
tosUrls: service.tosUrls,
|
||||||
|
kycnotmeUrl: `https://kycnot.me/service/${service.slug}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { accountActions } from './account'
|
import { accountActions } from './account'
|
||||||
import { adminActions } from './admin'
|
import { adminActions } from './admin'
|
||||||
|
import { apiActions } from './api'
|
||||||
import { commentActions } from './comment'
|
import { commentActions } from './comment'
|
||||||
import { notificationActions } from './notifications'
|
import { notificationActions } from './notifications'
|
||||||
import { serviceActions } from './service'
|
import { serviceActions } from './service'
|
||||||
@@ -19,6 +20,7 @@ import { serviceSuggestionActions } from './serviceSuggestion'
|
|||||||
export const server = {
|
export const server = {
|
||||||
account: accountActions,
|
account: accountActions,
|
||||||
admin: adminActions,
|
admin: adminActions,
|
||||||
|
api: apiActions,
|
||||||
comment: commentActions,
|
comment: commentActions,
|
||||||
notification: notificationActions,
|
notification: notificationActions,
|
||||||
service: serviceActions,
|
service: serviceActions,
|
||||||
|
|||||||
32
web/src/lib/endpoints.ts
Normal file
32
web/src/lib/endpoints.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { type ActionClient } from 'astro:actions'
|
||||||
|
|
||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
import type { z } from 'astro/zod'
|
||||||
|
|
||||||
|
export function makeEndpointFromAction<Action extends ActionClient<unknown, 'json', z.ZodType> & string>(
|
||||||
|
action: Action
|
||||||
|
): APIRoute {
|
||||||
|
return async (context) => {
|
||||||
|
try {
|
||||||
|
const input = await context.request.json()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
const result = await context.callAction(action, input)
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error('Error on endpoint', result.error)
|
||||||
|
return new Response(JSON.stringify({ error: result.error.message }), {
|
||||||
|
status: result.error.status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(result.data), {
|
||||||
|
status: 200,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error on endpoint', error)
|
||||||
|
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
||||||
|
status: 500,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -203,38 +203,9 @@ To **see comments waiting for moderation**, toggle the switch in the comments se
|
|||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
Access basic service data via our public API.
|
You can access basic service data via our public API.
|
||||||
|
|
||||||
**Attribution:** Please credit **KYCnot.me** if you use data from this API.
|
See the [API page](/docs/api) for more details.
|
||||||
|
|
||||||
### `GET /api/v1/service/[id]`
|
|
||||||
|
|
||||||
Fetches details for a single service.
|
|
||||||
|
|
||||||
- **`[id]`**: Can be a service ID, slug, name, or any registered URL (including .onion/.i2p).
|
|
||||||
|
|
||||||
**Example Requests:**
|
|
||||||
|
|
||||||
```
|
|
||||||
/api/v1/service/bisq
|
|
||||||
/api/v1/service/https://bisq.network
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example Response (200 OK):**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Bisq",
|
|
||||||
"description": "Decentralized Bitcoin exchange network.",
|
|
||||||
"kycLevel": 0,
|
|
||||||
"categories": ["exchange"],
|
|
||||||
"serviceUrls": ["https://bisq.network/"],
|
|
||||||
"onionUrls": [],
|
|
||||||
"i2pUrls": [],
|
|
||||||
"tosUrls": ["https://bisq.network/terms-of-service/"],
|
|
||||||
"kycnotmeUrl": "https://kycnot.me/service/bisq"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
|
|||||||
disabled: !user.karmaUnlocks.displayName,
|
disabled: !user.karmaUnlocks.displayName,
|
||||||
}}
|
}}
|
||||||
description={!user.karmaUnlocks.displayName
|
description={!user.karmaUnlocks.displayName
|
||||||
? `${makeKarmaUnlockMessage(karmaUnlocksById.displayName)} [Learn more](/karma)`
|
? `${makeKarmaUnlockMessage(karmaUnlocksById.displayName)} [Learn more](/docs/karma)`
|
||||||
: undefined}
|
: undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
|
|||||||
disabled: !user.karmaUnlocks.websiteLink,
|
disabled: !user.karmaUnlocks.websiteLink,
|
||||||
}}
|
}}
|
||||||
description={!user.karmaUnlocks.websiteLink
|
description={!user.karmaUnlocks.websiteLink
|
||||||
? `${makeKarmaUnlockMessage(karmaUnlocksById.websiteLink)} [Learn more](/karma)`
|
? `${makeKarmaUnlockMessage(karmaUnlocksById.websiteLink)} [Learn more](/docs/karma)`
|
||||||
: undefined}
|
: undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ const inputErrors = isInputError(result?.error) ? result.error.fields : {}
|
|||||||
square
|
square
|
||||||
disabled={!user.karmaUnlocks.profilePicture}
|
disabled={!user.karmaUnlocks.profilePicture}
|
||||||
description={!user.karmaUnlocks.profilePicture
|
description={!user.karmaUnlocks.profilePicture
|
||||||
? `${makeKarmaUnlockMessage(karmaUnlocksById.profilePicture)} [Learn more](/karma)`
|
? `${makeKarmaUnlockMessage(karmaUnlocksById.profilePicture)} [Learn more](/docs/karma)`
|
||||||
: undefined}
|
: undefined}
|
||||||
removeCheckbox={user.picture
|
removeCheckbox={user.picture
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -529,8 +529,9 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
|
|
||||||
<div class="border-night-500 bg-night-800/70 mb-4 rounded-md border px-4 py-3">
|
<div class="border-night-500 bg-night-800/70 mb-4 rounded-md border px-4 py-3">
|
||||||
<p class="text-day-300">
|
<p class="text-day-300">
|
||||||
Earn karma to unlock features and privileges. <a href="/karma" class="text-day-200 hover:underline"
|
Earn karma to unlock features and privileges. <a
|
||||||
>Learn about karma</a
|
href="/docs/karma"
|
||||||
|
class="text-day-200 hover:underline">Learn about karma</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
13
web/src/pages/api/[...catchAll].ts
Normal file
13
web/src/pages/api/[...catchAll].ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
|
||||||
|
export const ALL: APIRoute = (context) => {
|
||||||
|
console.error('Endpoint not found', { url: context.url.href, method: context.request.method })
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'Endpoint not found',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import { prisma } from '../../../../lib/prisma'
|
|
||||||
|
|
||||||
import type { Prisma } from '@prisma/client'
|
|
||||||
import type { APIRoute } from 'astro'
|
|
||||||
|
|
||||||
const MAX_ID_LENGTH = 2048
|
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ params }) => {
|
|
||||||
const { id } = params
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
return new Response(JSON.stringify({ error: 'ID parameter is missing' }), {
|
|
||||||
status: 400,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id.length > MAX_ID_LENGTH) {
|
|
||||||
return new Response(JSON.stringify({ error: 'ID parameter is too long' }), {
|
|
||||||
status: 400,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const orConditions: Prisma.ServiceWhereInput[] = [
|
|
||||||
{ slug: id },
|
|
||||||
{ name: id },
|
|
||||||
{ serviceUrls: { has: id } },
|
|
||||||
{ onionUrls: { has: id } },
|
|
||||||
{ i2pUrls: { has: id } },
|
|
||||||
]
|
|
||||||
|
|
||||||
// Try direct ID lookup first
|
|
||||||
const numericId = parseInt(id, 10)
|
|
||||||
|
|
||||||
if (!isNaN(numericId)) {
|
|
||||||
orConditions.push({ id: numericId })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id.startsWith('http://') || id.startsWith('https://')) {
|
|
||||||
let alternativeId: string
|
|
||||||
if (id.endsWith('/')) {
|
|
||||||
alternativeId = id.slice(0, -1) // Remove trailing slash
|
|
||||||
} else {
|
|
||||||
alternativeId = id + '/' // Add trailing slash
|
|
||||||
}
|
|
||||||
orConditions.push({ serviceUrls: { has: alternativeId } })
|
|
||||||
orConditions.push({ onionUrls: { has: alternativeId } })
|
|
||||||
orConditions.push({ i2pUrls: { has: alternativeId } })
|
|
||||||
} else {
|
|
||||||
// For non-HTTP/S IDs, check as is (could be a direct onion/i2p address without protocol)
|
|
||||||
orConditions.push({ serviceUrls: { has: id } })
|
|
||||||
orConditions.push({ onionUrls: { has: id } })
|
|
||||||
orConditions.push({ i2pUrls: { has: id } })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const service = await prisma.service.findFirst({
|
|
||||||
where: {
|
|
||||||
OR: orConditions,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
slug: true,
|
|
||||||
description: true,
|
|
||||||
kycLevel: true,
|
|
||||||
categories: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
slug: true,
|
|
||||||
icon: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
serviceUrls: true,
|
|
||||||
onionUrls: true,
|
|
||||||
i2pUrls: true,
|
|
||||||
tosUrls: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!service) {
|
|
||||||
return new Response(JSON.stringify({ error: 'Service not found' }), {
|
|
||||||
status: 404,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseData = {
|
|
||||||
name: service.name,
|
|
||||||
description: service.description,
|
|
||||||
kycLevel: service.kycLevel,
|
|
||||||
categories: service.categories.map((category) => category.slug),
|
|
||||||
serviceUrls: service.serviceUrls,
|
|
||||||
onionUrls: service.onionUrls,
|
|
||||||
i2pUrls: service.i2pUrls,
|
|
||||||
tosUrls: service.tosUrls,
|
|
||||||
kycnotmeUrl: `https://kycnot.me/service/${service.slug}`,
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(JSON.stringify(responseData), {
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching service:', error)
|
|
||||||
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
web/src/pages/api/v1/service/get.ts
Normal file
7
web/src/pages/api/v1/service/get.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { actions } from 'astro:actions'
|
||||||
|
|
||||||
|
import { makeEndpointFromAction } from '../../../../lib/endpoints'
|
||||||
|
|
||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
|
||||||
|
export const QUERY: APIRoute = makeEndpointFromAction(actions.api.service.get)
|
||||||
155
web/src/pages/docs/api.mdx
Normal file
155
web/src/pages/docs/api.mdx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
---
|
||||||
|
layout: ../../layouts/MarkdownLayout.astro
|
||||||
|
title: API
|
||||||
|
author: KYCnot.me
|
||||||
|
pubDate: 2025-05-31
|
||||||
|
description: 'Access basic service data via our public API.'
|
||||||
|
icon: 'ri:plug-line'
|
||||||
|
---
|
||||||
|
|
||||||
|
import { SOURCE_CODE_URL } from 'astro:env/server'
|
||||||
|
import { kycLevels } from '../../constants/kycLevels'
|
||||||
|
import { verificationStatuses } from '../../constants/verificationStatus'
|
||||||
|
|
||||||
|
Access basic service data via our public API.
|
||||||
|
|
||||||
|
All endpoints should be prefixed with `/api/v1/`.
|
||||||
|
|
||||||
|
The endpoints <a href={SOURCE_CODE_URL}>source code</a> is available on the `/web/src/actions/api/index.ts` file.
|
||||||
|
|
||||||
|
**Attribution:** Please credit **KYCnot.me** if you use data from this API.
|
||||||
|
|
||||||
|
## `QUERY` `/service/get`
|
||||||
|
|
||||||
|
Fetches details for a single service by various lookup criteria.
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| ------------ | ------ | -------- | ----------- |
|
||||||
|
| `id` | number | No* | Service ID |
|
||||||
|
| `slug` | string | No* | Service URL slug (lowercase letters, numbers, and hyphens only) |
|
||||||
|
| `serviceUrl` | string | No* | Service URL. May be web, onion, or i2p. May just be a domain or a full URL. |
|
||||||
|
|
||||||
|
\* At least one of the marked parameters is required.
|
||||||
|
|
||||||
|
### Response Format
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type ServiceResponse = {
|
||||||
|
id: number
|
||||||
|
slug: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
verificationStatus: 'VERIFICATION_SUCCESS' | 'APPROVED' | 'COMMUNITY_CONTRIBUTED' | 'VERIFICATION_FAILED'
|
||||||
|
verificationStatusInfo: {
|
||||||
|
value: 'VERIFICATION_SUCCESS' | 'APPROVED' | 'COMMUNITY_CONTRIBUTED' | 'VERIFICATION_FAILED'
|
||||||
|
slug: string
|
||||||
|
label: string
|
||||||
|
labelShort: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
kycLevel: 0 | 1 | 2 | 3 | 4
|
||||||
|
kycLevelInfo: {
|
||||||
|
value: 0 | 1 | 2 | 3 | 4
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
categories: {
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
}[]
|
||||||
|
serviceUrls: string[]
|
||||||
|
tosUrls: string[]
|
||||||
|
kycnotmeUrl: `https://kycnot.me/service/${service.slug}`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### KYC Levels
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{kycLevels.map((level) => (
|
||||||
|
<li key={level.id}>
|
||||||
|
<strong>{level.id}</strong>: {level.name} - {level.description}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
### Verification Status
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{verificationStatuses.map((status) => (
|
||||||
|
<li key={status.value}>
|
||||||
|
<strong>{status.value}</strong>: {status.description}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```zsh
|
||||||
|
curl -X QUERY https://kycnot.me/api/v1/service/get \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"slug": "my-example-service"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "My Example Service",
|
||||||
|
"description": "This is a description of my example service",
|
||||||
|
"verificationStatus": "VERIFICATION_SUCCESS",
|
||||||
|
"verificationStatusInfo": {
|
||||||
|
"value": "VERIFICATION_SUCCESS",
|
||||||
|
"slug": "verified",
|
||||||
|
"label": "Verified",
|
||||||
|
"labelShort": "Verified",
|
||||||
|
"description": "Thoroughly tested and verified by the team. But things might change, this is not a guarantee."
|
||||||
|
},
|
||||||
|
"kycLevel": 0,
|
||||||
|
"kycLevelInfo": {
|
||||||
|
"value": 0,
|
||||||
|
"name": "Guaranteed no KYC",
|
||||||
|
"description": "Terms explicitly state KYC will never be requested."
|
||||||
|
},
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"name": "Exchange",
|
||||||
|
"slug": "exchange"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"serviceUrls": [
|
||||||
|
"https://example.com",
|
||||||
|
"http://c9ikae0fdidzh1ufrzp022e5uqfvz6ofxlkycz59cvo6fdxjgx7ekl9e.onion"
|
||||||
|
],
|
||||||
|
"tosUrls": ["https://example.com/terms-of-service"],
|
||||||
|
"kycnotmeUrl": "https://kycnot.me/service/bisq"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Responses
|
||||||
|
|
||||||
|
**404 Not Found**: Service not found
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Service not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**400 Bad Request**: Invalid input parameters
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Validation error message"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**500 Internal Server Error**: Server error
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Internal server error"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
layout: ../layouts/MarkdownLayout.astro
|
layout: ../../layouts/MarkdownLayout.astro
|
||||||
title: How does karma work?
|
title: How does karma work?
|
||||||
description: "KYCnot.me has a user karma system, here's how it works"
|
description: "KYCnot.me has a user karma system, here's how it works"
|
||||||
icon: 'ri:hearts-line'
|
icon: 'ri:hearts-line'
|
||||||
@@ -7,7 +7,7 @@ author: KYCnot.me
|
|||||||
pubDate: 2025-05-15
|
pubDate: 2025-05-15
|
||||||
---
|
---
|
||||||
|
|
||||||
import KarmaUnlocksTable from '../components/KarmaUnlocksTable.astro'
|
import KarmaUnlocksTable from '../../components/KarmaUnlocksTable.astro'
|
||||||
|
|
||||||
[KYCnot.me](https://kycnot.me) implements a karma system to encourage quality contributions and maintain community standards. Users can earn (or lose) karma points through various interactions on the platform, primarily through their comments on services.
|
[KYCnot.me](https://kycnot.me) implements a karma system to encourage quality contributions and maintain community standards. Users can earn (or lose) karma points through various interactions on the platform, primarily through their comments on services.
|
||||||
|
|
||||||
@@ -647,8 +647,9 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
|
|||||||
|
|
||||||
<div class="border-night-500 bg-night-800/70 mb-4 rounded-md border px-4 py-3">
|
<div class="border-night-500 bg-night-800/70 mb-4 rounded-md border px-4 py-3">
|
||||||
<p class="text-day-300">
|
<p class="text-day-300">
|
||||||
Earn karma to unlock features and privileges. <a href="/karma" class="text-day-200 hover:underline"
|
Earn karma to unlock features and privileges. <a
|
||||||
>Learn about karma</a
|
href="/docs/karma"
|
||||||
|
class="text-day-200 hover:underline">Learn about karma</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user